Tech 4 min read

Firefox 148 ships Sanitizer API, letting setHTML safely replace innerHTML

IkesanContents

XSS has stayed near the top of the Web vulnerability rankings for almost a decade. Mozilla proposed Content Security Policy back in 2009 as a root-level defense, but CSP never became universal because it forces large architectural changes on existing sites.

That changed with Firefox 148, released on February 24, 2026. Firefox became the first browser to ship the WICG Sanitizer API in its safe form. With setHTML(), which inserts HTML through a built-in sanitizer, developers can block XSS with minimal code changes.

what is wrong with innerHTML

innerHTML inserts HTML directly into the DOM. If you pass user input or external data through unchecked, attacker-controlled scripts can execute.

// dangerous
element.innerHTML = userInput;

The usual fix is to sanitize with a third-party library such as DOMPurify, but library bugs and bypasses have been found many times. Moving that logic into the browser removes a lot of the burden from application code.

how setHTML() works

Mozilla Hacks showed this example:

document.body.setHTML(`<h1>Hello my name is <img src="x" onclick="alert('XSS')">`);

The call strips the <img> element and its onclick handler, leaving only safe HTML:

<h1>Hello my name is</h1>

The default sanitizer is stricter than many people expect. It removes not only obvious script-bearing elements such as <script>, <iframe>, <embed>, <object>, and SVG <use>, but also event-handler attributes like onclick and onerror, javascript: URLs, and even many elements that are commonly safe in normal content. Only ordinary text content is kept by default.

custom configuration

If the default rules are too strict or too loose, you can pass a custom sanitizer as the second argument.

const sanitizer = new Sanitizer({
  allowElements: ['p', 'b', 'i', 'a', 'ul', 'li'],
  allowAttributes: {
    'a': ['href']
  }
});
element.setHTML(userInput, { sanitizer });

Useful options include:

  • allowElements
  • allowAttributes
  • blockElements
  • dropElements

For a rich text editor you might allow <table> or <pre><code>, while a comments field may need only a very small allowlist. You can test behavior in the Sanitizer API Playground.

Even if your custom configuration allows <script>, setHTML() still strips it. The baseline XSS protections are not overridable.

setHTMLUnsafe()

There is also setHTMLUnsafe(), which does not enforce the XSS baseline.

element.setHTMLUnsafe(`
  <div>
    <template shadowrootmode="open">
      <p>Shadow DOM content</p>
    </template>
  </div>
`);

setHTML() removes <template>, so Declarative Shadow DOM requires setHTMLUnsafe(). The name sounds scary, but when paired with Trusted Types it can still be used safely.

setHTMLUnsafe() shipped earlier in Chrome 124 and Safari 17.4, and Firefox 148 adds it as well. It reached Baseline 2025 Newly available in September 2025.

methodXSS baseline enforcedDeclarative Shadow DOMTrusted Types sink
setHTML()alwaysnono
setHTMLUnsafe()noyesyes

Trusted Types

Trusted Types adds another layer by limiting dangerous DOM sinks such as innerHTML to TrustedHTML values. If a site already uses setHTML(), it becomes much easier to turn on a strict Trusted Types policy and block accidental unsafe insertions elsewhere.

Firefox 148 supports both Sanitizer API and Trusted Types.

why DOMPurify is structurally incomplete

Libraries like DOMPurify parse HTML, sanitize it, serialize it, and then let the browser parse it again. That double parse makes mutation-based XSS possible. DOMPurify has had several bypasses in the past:

  • DOMPurify 2.0.0: MathML namespace confusion
  • DOMPurify 2.0.17: another namespace confusion bug

setHTML() filters directly against the browser’s real HTML parser, so parser mismatches disappear. That makes mXSS structurally harder, which is the key difference from library-based sanitization.

the odd history of the spec

Sanitizer API standardization has been messy:

  • 2020: WICG started the browser-native HTML sanitization effort
  • 2022: Chrome 105 shipped the original draft API
  • 2023: the spec was redesigned and Chrome 119 removed the old API
  • 2024: setHTMLUnsafe() and parseHTMLUnsafe() shipped in Chrome 124 and Safari 17.4
  • February 2026: Firefox 148 and Chrome 146 beta shipped the safe setHTML() plus Sanitizer options

The spec is still in WICG draft form, with eventual integration into WHATWG HTML planned.

browser support

Support for the safe setHTML() + Sanitizer combo:

browserversionstatus
Firefox148shipped
Chrome146 betabeta, stable planned for early March 2026
Edgeequivalent to Chrome 146expected around the same time
Safarinot implementedpositive standards position, timing unknown

Until Safari catches up, feature detection and a fallback are the pragmatic path:

function safeInsertHTML(element, html) {
  if ("setHTML" in Element.prototype) {
    element.setHTML(html);
  } else {
    element.innerHTML = DOMPurify.sanitize(html);
  }
}

adoption path

Migration is fairly straightforward. In many places you only need to replace innerHTML assignments with setHTML().

// before
element.innerHTML = sanitizedHtml;

// after
element.setHTML(html);  // the browser handles sanitization

Unlike CSP, this does not require a site-wide architecture change. You can roll it out gradually, one dangerous sink at a time.

The limitation is that setHTML() is a browser API, so it is not available on the server. Server-side sanitization still needs something like DOMPurify plus jsdom. And because setHTML() mutates the DOM instead of returning a sanitized string, it is not a drop-in replacement for every DOMPurify use case.