Security

Zoijs is secure by default. The safe path is the only path you'll normally use — you have to go out of your way to do something dangerous, and several dangerous things are simply blocked.

Threat model#

Untrusted data (user input, API responses, URL params, stored content) flows into your templates. The goal: that data can never become executable script, markup, an event handler, or a dangerous URL.

The core guarantee: **dynamic values fill text and attribute slots only — they can never change a template's structure.** The template scanner keeps your static HTML and your ${} values in separate channels and refuses to put a value where a tag name, attribute name, or raw-HTML sink would go.

Safe rendering rules#

What you writeWhat happensSafe?
${() => value} in textrendered as an inert Text node (escaped)✅ always
attr=${() => value}set via setAttribute (or property for value/checked)
URL attrs (href, src, action, formaction, poster, ping, data, xlink:href)scheme-checked✅ unsafe schemes blocked
onclick=${fn}addEventListener with a function reference✅ strings ignored

Text is always escaped#

html`<p>${() => userInput}</p>`;
// userInput = "<img src=x onerror=alert(1)>"  →  shown as literal text. No element, no execution.

URLs are scheme-validated#

Allowed: http, https, mailto, tel, relative URLs, and raster data:image/* (png/jpeg/gif/webp/avif/bmp/ico). Blocked: javascript:, vbscript:, data:text/html, data:image/svg+xml, and any unknown scheme. The check runs on every URL-bearing attribute — including data (the <object data> sink, which loads a nested browsing context) — and strips control characters first, so tricks like java\tscript: don't slip through.

html`<a href=${() => url}>link</a>`;
// url = "javascript:alert(1)"  →  href is not set.

Event handlers are functions, never strings#

html`<button onclick=${doThing}>x</button>`;     // ✅ function reference
html`<button onclick=${"doThing()"}>x</button>`; // ⚠️ ignored — a string is never wired up or eval'd

Unsafe patterns to avoid#

These either throw a clear error or are blocked:

PatternResultDo this instead
<${tag}> (dynamic tag)throwsuse a conditional returning different templates
<el ${x}> (dynamic/spread attribute name)throwsname attributes statically: disabled=${cond}
<iframe srcdoc=${html}>attribute blockeddon't inject HTML; build real elements
<textarea>${x}</textarea>throwsbind the property in code
onclick="a ${fn}" (multi-part handler)throwsonclick=${fn}
el.innerHTML = data (your own code)bypasses Zoijs entirelynever assign untrusted data to innerHTML

There is intentionally no raw-HTML rendering API in Zoijs. If you genuinely need to render rich HTML you broadly trust (e.g. markdown or CMS output), use the optional @zoijs/sanitize package: sanitize(dirtyHtml) parses the string inertly and returns safe DOM nodes (allowlist-based, reusing the same URL/attribute guards described above) that drop straight into a text binding — html\<article>${() => sanitize(body)}</article>\``. For fully adversarial input in high-value contexts, prefer a dedicated, independently-audited sanitizer (e.g. DOMPurify) and treat that boundary as security-critical.

A note on style#

Binding style=${...} from an untrusted string is risky (CSS can exfiltrate data with background:url(…) or enable clickjacking). Prefer the object form, which is injection-safe:

html`<div style=${() => ({ width: pct + "%", color: theme })}>…</div>`;

Keys and values come from an object, not string concatenation, so a value can never break out of the attribute or inject extra declarations. Zoijs still allows a dynamic string style (legitimate cases exist) — bind it only from data you control; in dev mode a string style containing a risky token (url(, expression(, a CSS comment) logs a one-time warning nudging you to the object form.

A note on returning DOM nodes#

A text binding can return a DOM Node you constructed. Zoijs inserts it as-is — so if your code builds a <script> node from untrusted input and returns it, that's on you. Build nodes only from trusted data.

CSP compatibility#

Zoijs is friendly to a strict Content Security Policy:

  • No eval / new Function anywhere → no 'unsafe-eval' needed.
  • No inline scripts or inline event handlers are injected → no 'unsafe-inline' needed for scripts.
  • Trusted Types (require-trusted-types-for 'script'): Zoijs uses <template>.innerHTML with framework-generated HTML built only from your static template strings (never data). Under Trusted Types it routes that through a pass-through policy named zoijs, which is safe by construction. Allow it in your CSP:

`` Content-Security-Policy: require-trusted-types-for 'script'; trusted-types zoijs; ``

A recommended baseline:

Content-Security-Policy: default-src 'self'; script-src 'self'; require-trusted-types-for 'script'; trusted-types zoijs;

Summary#

  • Text → inert, escaped. URLs → scheme-checked. Handlers → functions only.
  • on* and srcdoc attributes are blocked from data; dynamic tag/attribute names throw.
  • No eval, no Virtual DOM, no raw-HTML API. CSP- and Trusted-Types-friendly.
  • The one rule that keeps you safe: let Zoijs render your data — never hand untrusted data to innerHTML yourself.