@zoijs/sanitize
v0.1.0
Published
Turn an untrusted HTML string into safe DOM nodes for Zoijs — allowlist-based, zero-dependency, reuses the core's URL/attribute guards. No JSX, no build step.
Downloads
36
Maintainers
Readme
@zoijs/sanitize
Turn an untrusted HTML string into safe DOM nodes for Zoijs. Allowlist-based, zero-dependency, and it reuses the core's own URL/attribute guards.
@zoijs/sanitize is an optional package. Zoijs has no raw-HTML API by design — a string in a text slot is inert text, and there is deliberately no unsafeHTML. But real apps still need to render rich HTML they mostly trust: the output of a markdown renderer, a CMS body, an email preview. That boundary is the single most security-critical spot in a front end. sanitize() makes it a supported, tested path instead of one you solve alone.
Install
npm install @zoijs/core @zoijs/sanitizeOr with no install, from a CDN:
import { sanitize } from "https://esm.sh/@zoijs/[email protected]";What sanitize() does
sanitize(dirtyHtml) takes an untrusted HTML string and returns an array of safe DOM nodes. It drops straight into a Zoijs text binding — a binding renders returned nodes as-is and tracks them for clean removal on update:
import { html, mount } from "@zoijs/core";
import { sanitize } from "@zoijs/sanitize";
function Post({ bodyHtml }) {
return html`<article>${() => sanitize(bodyHtml)}</article>`;
}
mount(() => Post({ bodyHtml: renderedMarkdown }), "#app");Everything dangerous is removed; the safe rich text is kept:
sanitize('<p onclick="steal()">Hi <strong>there</strong> <script>evil()</script></p>');
// → nodes for: <p>Hi <strong>there</strong></p>Why it's safe
- Inert parse. The string is parsed with
DOMParserinto a document that is never connected — so no script runs and no resource loads during parsing. - Allowlist, not denylist. Only known-safe elements and attributes survive. Everything else —
script,style,iframe,object,embed, SVG/MathML, everyon*handler,srcdoc, unknown tags — is removed. A denylist can't keep up with parser quirks; an allowlist can. - Same URL guard as the core.
href/src/citeare scheme-checked with the sameisSafeUrlthe Zoijs renderer uses, sojavascript:anddata:text/htmlcan't slip through — and the decision can never drift from the rest of the framework. - Reverse-tabnabbing guard.
target="_blank"links getrel="noopener noreferrer".
Because it returns live DOM nodes (not a string), there is no HTML sink for the browser to re-parse — the safe path stays the only path.
What survives
Elements — text and structural content only:
a abbr address article aside b bdi bdo blockquote br caption cite code col colgroup dd del details dfn div dl dt em figcaption figure footer h1–h6 header hgroup hr i img ins kbd li main mark nav ol p pre q rp rt ruby s samp section small span strong sub summary sup table tbody td tfoot th thead time tr u ul var wbr
Attributes — class, id, title, dir, lang, role, any aria-* / data-*, plus per-element ones like href, src, alt, colspan, datetime, cite. The style attribute is dropped (CSS is an injection surface — bind style through Zoijs's object form instead).
Anything not on these lists is removed.
The API
| Function | Purpose |
|---|---|
| sanitize(dirty) | Sanitize an untrusted HTML string → Node[] (safe DOM nodes). null/undefined/"" → []. |
That's the whole package — one function.
Server rendering
sanitize() returns live DOM nodes, so it is a client helper — it needs a browser DOM and throws without one. Sanitized content is therefore rendered on the client (it hydrates after load); it is not part of @zoijs/ssr string output. If you need sanitized rich text in your server-rendered HTML for SEO, sanitize to a string with a server-side tool in your render pipeline and place it in your shell yourself.
Threat model & limits
This is a conservative allowlist sanitizer for rich text you broadly trust (your own markdown/CMS pipeline). It forbids foreign content (SVG/MathML) and raw-text elements, which removes the common mutation-XSS classes, and it reuses the core's vetted URL/attribute predicates.
It is not a drop-in replacement for a dedicated, independently-audited sanitizer when the input is fully adversarial and the stakes are high. If you are sanitizing arbitrary attacker-controlled HTML in a high-value context, use a specialized, security-audited library (e.g. DOMPurify) and treat that boundary as security-critical — the same advice the core security guide gives.
Never assign untrusted data to innerHTML yourself: that bypasses Zoijs (and this package) entirely.
License
MIT © Zoijs contributors
