Limitations and traps
Traps first: the gaps and surprises a reader cannot recover from the API alone. Each known gap has an issue. The package is
published as 0.0.0 (2026-10-03; its security review was signed off by the maintainer on 2026-10-01, safe-fragment#1); read this page before relying on it. The positive guarantees are on
Security model.
Known gaps
Section titled “Known gaps”Each has an issue in johnhenry/safe-fragment.
- No independent security review yet (safe-fragment#1). Do not release or rely on this for hostile content before it.
email-v1is a scaffold:cid:, Outlook VML and MSO conditional comments are unhandled, and it has no email-specific corpus (safe-fragment#2).- No SVG/MathML in any profile (safe-fragment#3). They are dropped with their whole subtree.
- The native Sanitizer API spec is still moving; only Chromium (and, per CI, Firefox) ship
setHTML, and Safari takes the DOMPurify path (safe-fragment#4). - DOMPurify’s cost is quadratic in removed nodes:
maxInputLengthbounds it, it does not remove it (safe-fragment#5). Lower it if you render attacker-sized content in Safari. article-v1/ui-v1keep relativeimg src, a credentialed same-origin GET the moment the content renders; opt in toblockRelativeAutoLoadUrls(safe-fragment#6).ui-v1allowsclass, which can match host selectors (safe-fragment#7).<style>is not supported in any profile (dropped with its content): sanitizing CSS is a separate, larger security surface (ADR 0006, safe-fragment#11). Keep component stylesheets outside the sanitized template; see Styles.- The native path’s report cannot count the engine’s own unconditional removals (
<script>,<iframe>,on*handlers,javascript:URLs; safe-fragment#8, ADR 0007): counting them needs a Trusted-Types-gated parse, which this package never makes, so sanitizing produces zero CSP violations. It lists everything the profile removed, and on DOMPurify the log also includes those baseline removals.
Rendering
Section titled “Rendering”- There is no default profile. A missing or unknown
profilerejects withUNKNOWN_PROFILE; nothing renders. - A rejected render clears what was on screen. Content never stays under a profile or source the element no longer claims.
The one exception is a
before-renderveto (RENDER_ABORTED), which leaves existing content alone. render()never rejects. Checkresult.status, not acatch.supersededis not a failure, and emits no event.- A disallowed element’s text survives; a dangerous container’s content never does.
<marquee>b</marquee>becomesb, but the contents of<script>,<style>,<template>,<textarea>and the like are dropped with the element (ADR 0004). - Unregistered custom elements are unwrapped, not kept. Allow them by deriving a profile, and register your element definitions yourself.
- Ids are rewritten. Every surviving
idis prefixeduser-content-, sodocument.getElementById("x")will not find content authored asid="x". Opt out withidPolicy: "keep-in-shadow"only when the fragment goes into a shadow root (<safe-fragment>rejects it withINVALID_OPTIONunderscope="light";sanitizeToFragmentcannot check, so it is yours).nameon elements that create named properties is namespaced under every policy. - A git install needs the
dompurifyimport map too.preparebuildsdist/(getting started), butdist/index.jsstill does a bareimport("dompurify"). <button>is alwaystype="button",data-*is an allowlist.data-actionis the only oneui-v1allows.scope="shadow"is not isolation (ADR 0003).- The legacy
contentattribute is the lowest-precedence source and logs aconsole.warn. outputLengthin the report is approximate.
Sanitizer loading
Section titled “Sanitizer loading”- No bundler, no import map: Safari renders nothing. The fallback’s
import("dompurify")is a bare specifier. Without a mapping the render rejects withSANITIZER_UNAVAILABLE. See the import map, and pin the exactdompurifyversion the release pins. sanitizeToFragmentSyncthrowsSANITIZER_NOT_READYwhere no engine is ready synchronously. It fails closed. Callawait preloadSanitizer()first.- On raw-file CDNs, list
dompurifyexplicitly when generating an import map with mport, or pass--dependencies(build(specs, { dependencies: true })) to let mport add it.
Profiles
Section titled “Profiles”- Built-in profiles cannot be modified or replaced.
registerProfileon a built-in or already-registered name throwsINVALID_PROFILE; derive a new name. - A
-v<N>name suffix must matchversion. Changing the suffix when deriving without settingversionfails withPROFILE_MISMATCH. - A profile can only narrow or extend within the validated envelope. No dangerous elements,
on*/style, or dangerous schemes can be registered, and there is no wildcarddata-*. deriveProfilereplacescustomElementsrather than merging them.
Remote src
Section titled “Remote src”srcis disabled by default. Without the fetch capability enabled it rejects withFETCH_DISABLED.- Redirects are refused by default, and with
followRedirectsthe final origin is re-checked. FETCH_ABORTEDandFETCH_SUPERSEDEDare only on therender()result, never events: an abort you caused is not a failure.
Packaging
Section titled “Packaging”- Loading both the ESM and CJS builds is supported. The profile registry, DOMPurify loader and instance cache live on a
Symbol.forkey onglobalThisand are shared by both builds, so a profile registered through one is visible to the other, andinstanceof SafeFragmentErrorworks across them. Do not add other module-level mutable state if you fork. loading="lazy"falls back to eager rendering whenIntersectionObserveris missing, rather than never rendering.
What is solid
Section titled “What is solid”Implemented and covered by the Vitest Browser Mode suite (real Chromium, WebKit and Firefox; the Firefox run is CI-only), including the adversarial XSS corpus and a benign-content corpus compared across both sanitization engines:
- The sanitizer pipeline (both engines,
enforceProfile, rebuild), the report, and the publicsanitizeToFragmentAPI. plain-text-v1,article-v1,ui-v1,component-template-v1; custom profiles viaregisterProfile.<safe-fragment>’s lifecycle,render()results, events, shadow and light scope,loading="lazy".- The
srcfetch capability model. <example-sandbox>, including a direct isolation-proof test and Trusted Types support.
“Solid” means tested by the project’s own suite, not independently reviewed (safe-fragment#1).
Not goals
Section titled “Not goals”- A trusted or unsafe fast path. Assign trusted markup yourself with the platform’s own APIs, in your own code.
- Detecting phishing links and other content-level abuse.
- Isolating or sandboxing what your own custom elements do.
- Running or vetting executable code; that is
<example-sandbox>, for trusted samples only.