opdeck / blog / preventing-xss-vulnerabilities-with-sanitizer-api

How to Use Firefox's Sanitizer API for Better XSS Protection

September 7, 2026 / OpDeck Team
XSS ProtectionWeb SecurityFirefox APISanitizerVulnerability Management

Why XSS Still Haunts Modern Web Applications

Cross-site scripting has been on the OWASP Top 10 list for well over a decade, and despite all the advances in frontend frameworks, build tooling, and browser security, it remains one of the most exploited vulnerability classes on the web. The reason is almost embarrassingly simple: developers need to inject dynamic HTML into the DOM, and the most obvious tool for doing that — innerHTML — is a loaded gun with no safety.

If you've ever written something like this:

document.getElementById('output').innerHTML = userInput;

You've potentially opened a door to script injection. Even sanitization libraries, which many teams rely on as a safety net, have historically been bypassable with sufficiently creative payloads. The browser has always been better positioned to handle this safely than a third-party JavaScript library, but until now, there was no standardized browser-native API to do it.

Firefox 148 changes that with the first shipping implementation of the standardized Sanitizer API, and with it comes a new DOM method: setHTML().


What Is the Sanitizer API?

The Sanitizer API is a browser-native HTML sanitization interface that allows developers to safely parse and insert untrusted HTML into the DOM without risking script execution. It was developed through the W3C and WHATWG standardization process and is now landing in Firefox 148 as the first stable browser to ship the finalized version.

At its core, the API provides two things:

  1. A Sanitizer class — which you can configure to define what HTML elements and attributes are allowed or disallowed.
  2. A setHTML() method on Element — which parses a string of HTML through the sanitizer and inserts the result into the DOM, all in one atomic operation.

The key distinction from innerHTML is that setHTML() never allows script execution. Even if a malicious payload somehow bypasses your sanitizer configuration, the method itself has built-in protections that prevent <script> tags and inline event handlers from executing.


The Problem with innerHTML (and Why Libraries Aren't Enough)

To appreciate why setHTML() matters, it's worth understanding exactly what makes innerHTML dangerous and why third-party sanitization libraries are an imperfect solution.

innerHTML Parses HTML Eagerly

When you assign a string to innerHTML, the browser immediately parses it as HTML. This includes:

  • <script> tags (which may not execute due to browser heuristics, but this is inconsistent)
  • Inline event handlers like onclick, onerror, onload
  • Data URIs in href or src attributes
  • SVG and MathML elements that can host scripts in unexpected ways

A classic bypass that trips up many sanitization libraries looks like this:

<img src="x" onerror="alert(document.cookie)">

Or more sophisticated variations using SVG namespaces:

<svg><animate onbegin="alert(1)" attributeName="x" dur="1s"></svg>

Library-Based Sanitization Has an Attack Surface

DOMPurify is the gold standard of client-side sanitization libraries, and it's genuinely excellent. But it's still JavaScript running in the same context as everything else on your page. It has to be kept up to date, it can be bypassed by mutation-based XSS (mXSS) attacks that exploit differences between the HTML parser used during sanitization and the one used during actual DOM insertion, and it adds to your JavaScript bundle size and dependency surface.

The Sanitizer API solves the mXSS problem structurally: because setHTML() uses the same HTML parser that will ultimately render the content, there's no parsing discrepancy to exploit.


How to Use setHTML() and the Sanitizer API

Let's look at practical usage, starting with the most basic form and working up to custom configurations.

Basic Usage with Default Sanitizer

The simplest way to use the new API:

const userContent = '<p>Hello <b>world</b>!</p><script>alert("xss")</script>';
const container = document.getElementById('output');

container.setHTML(userContent);

With the default sanitizer, the <script> tag is stripped automatically. The result in the DOM will be:

<p>Hello <b>world</b>!</p>

No configuration needed, no library to import, no bundle to maintain.

Using a Custom Sanitizer Instance

If you need tighter control — for example, you're building a comment system that should only allow basic text formatting — you can create a Sanitizer instance with an explicit allowlist:

const sanitizer = new Sanitizer({
  allowElements: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
  allowAttributes: {
    'a': ['href', 'title'],
  },
  blockElements: ['div', 'span'],
  dropElements: ['script', 'style', 'iframe'],
});

const container = document.getElementById('comment-body');
container.setHTML(userInput, { sanitizer });

The configuration options give you granular control:

  • allowElements — only these elements will be kept; everything else is stripped
  • blockElements — these elements are removed but their children are kept
  • dropElements — these elements and all their children are removed entirely
  • allowAttributes — a map of elements to their permitted attributes

Feature Detection

Since the Sanitizer API is currently only in Firefox 148 (with other browsers expected to follow), you'll want to feature-detect before using it:

if ('setHTML' in Element.prototype) {
  container.setHTML(userInput, { sanitizer });
} else {
  // Fall back to DOMPurify or another sanitization approach
  container.innerHTML = DOMPurify.sanitize(userInput);
}

This progressive enhancement pattern lets you take advantage of the native API where available while maintaining compatibility everywhere else.

Using setHTMLUnsafe() for Trusted Content

The Sanitizer API also introduces setHTMLUnsafe(), which inserts HTML without sanitization. Despite the alarming name, it has legitimate uses — for example, inserting server-rendered HTML that you've already verified is safe, or working with declarative shadow DOM. The name is intentionally scary to make developers pause and think before using it.

// Only use this with content you fully control and trust
container.setHTMLUnsafe(trustedServerRenderedHTML);

Sanitizer API vs. Content Security Policy: Complementary, Not Competing

A common question is whether the Sanitizer API replaces Content Security Policy (CSP). The short answer is no — they address different threat vectors and work best together.

CSP is a server-side control that tells the browser which sources of scripts, styles, and other resources are permitted. A strict CSP with script-src 'none' or script-src 'self' can block many XSS attacks at the execution layer, even if malicious code is injected into the DOM.

The Sanitizer API operates at the injection layer — it prevents malicious content from reaching the DOM in the first place.

Using both is the defense-in-depth approach:

  1. Sanitize user input before DOM insertion with setHTML()
  2. Enforce CSP headers to block unexpected script execution
  3. Use Trusted Types (another modern browser security API) to enforce sanitization at assignment points throughout your codebase

A solid CSP configuration in your server response headers looks like:

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';

You can verify whether your site is sending proper security headers — including CSP — using the Vulnerability Scanner on OpDeck, which checks for missing or misconfigured security headers, XSS exposure indicators, and other common security issues.


The Broader Security Picture: What Else You Should Be Checking

The Sanitizer API is a meaningful step forward, but XSS is just one item on a security checklist. Here's what a thorough security audit of a modern web application should cover.

SSL/TLS Configuration

All of your carefully sanitized content is worthless if it's being served over an insecure connection. An attacker performing a man-in-the-middle attack on an HTTP connection can inject scripts before the page even reaches the user's browser — no DOM manipulation required.

You should verify that your SSL certificate is valid, properly chained, uses strong cipher suites, and isn't close to expiration. The SSL Certificate Checker on OpDeck gives you a detailed breakdown of your certificate configuration, including expiration dates, issuer chain, and protocol support.

HTTP Security Headers

Beyond CSP, there are several other security-relevant HTTP headers that significantly reduce your attack surface:

  • X-Content-Type-Options: nosniff — prevents MIME type sniffing attacks
  • X-Frame-Options: DENY or Content-Security-Policy: frame-ancestors 'none' — prevents clickjacking
  • Strict-Transport-Security — enforces HTTPS connections
  • Permissions-Policy — restricts access to browser features like camera, microphone, geolocation
  • Referrer-Policy — controls what information is sent in the Referer header

Many of these headers are still missing from production sites. Running a security scan with the Vulnerability Scanner will surface which of these are absent or misconfigured.

Dependency Vulnerabilities

If you're using npm packages — and virtually every modern web application does — you have a transitive dependency chain that could introduce vulnerabilities. npm audit is a baseline, but it only catches known CVEs. Keeping your dependencies updated and monitoring for new disclosures is an ongoing process.


Performance Implications of the Sanitizer API

One underappreciated benefit of the native Sanitizer API is performance. DOMPurify, for all its quality, is still a JavaScript library that parses HTML in a sandboxed DOM fragment before cleaning it. The native Sanitizer API is implemented in the browser's C++ engine, which means it's substantially faster for high-volume use cases.

This matters in applications that render large amounts of user-generated content — social platforms, content management systems, collaborative editors, or any interface where dozens or hundreds of user-contributed HTML fragments are rendered on a single page load.

If you're rendering user content at scale and seeing performance bottlenecks in your JavaScript profiler, switching to the native Sanitizer API (where available) is a meaningful optimization. You can measure the real-world impact on your page's overall performance using the Website Performance Analyzer, which runs a Lighthouse-based audit and surfaces JavaScript execution time, main thread blocking, and Time to Interactive metrics.


Preparing Your Codebase for the Sanitizer API

If you want to start adopting setHTML() today, here's a practical migration approach.

Step 1: Audit Your innerHTML Usage

Search your codebase for all assignments to innerHTML, outerHTML, and insertAdjacentHTML. These are your injection points:

grep -rn "innerHTML\|outerHTML\|insertAdjacentHTML" src/

For each occurrence, determine whether the content being inserted is:

  • Fully developer-controlled (static strings, template literals with no user input) — these are generally safe
  • Derived from user input or external data — these need sanitization

Step 2: Categorize and Prioritize

Not all injection points carry equal risk. An injection point that receives data from a third-party API is higher risk than one that receives data from your own authenticated backend. Prioritize accordingly.

Step 3: Implement Progressive Enhancement

For each high-risk injection point, implement the feature-detected pattern:

function safeSetHTML(element, html, sanitizerConfig = {}) {
  if ('setHTML' in Element.prototype) {
    const sanitizer = Object.keys(sanitizerConfig).length > 0
      ? new Sanitizer(sanitizerConfig)
      : undefined;
    element.setHTML(html, sanitizer ? { sanitizer } : {});
  } else {
    // Fallback: use DOMPurify with equivalent config
    element.innerHTML = DOMPurify.sanitize(html, {
      ALLOWED_TAGS: sanitizerConfig.allowElements,
      ALLOWED_ATTR: Object.values(sanitizerConfig.allowAttributes || {}).flat(),
    });
  }
}

Step 4: Add Trusted Types Enforcement

If you want to go further and prevent any future developer from accidentally bypassing your sanitization, enable Trusted Types via CSP:

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

This causes the browser to throw an error if any code attempts to assign an unsanitized string to a dangerous sink like innerHTML, forcing all such assignments to go through a declared Trusted Types policy.


What's Coming Next: Browser Adoption and the Road Ahead

Firefox 148 is the first stable release to ship the finalized Sanitizer API specification, but it won't be alone for long. Chromium-based browsers have had experimental implementations behind flags for some time, and the standardization work is mature enough that broad adoption across major browsers is a reasonable near-term expectation.

The Sanitizer API is also being designed to integrate cleanly with other modern security primitives:

  • Trusted Types — the Sanitizer API can be used as the implementation behind a Trusted Types policy
  • Declarative Shadow DOMsetHTMLUnsafe() supports shadow DOM templates
  • Speculation Rules and prerendering — safe HTML injection is increasingly relevant as browsers do more speculative work ahead of navigation

The web security model is becoming more layered and more browser-native, and the Sanitizer API is a significant piece of that architecture.


Wrapping Up

The arrival of setHTML() and the Sanitizer API in Firefox 148 represents a meaningful shift in how the web platform handles one of its oldest security problems. By moving sanitization into the browser engine itself, the API eliminates entire categories of attack — including mutation-based XSS — that have historically been difficult to address with JavaScript libraries alone.

For developers, the path forward is clear: audit your innerHTML usage, adopt setHTML() with progressive enhancement, pair it with a strong Content Security Policy, and treat it as one layer in a defense-in-depth security posture rather than a silver bullet.

Security is never a single fix — it's an ongoing process of auditing, monitoring, and improvement. OpDeck provides a range of tools to help you stay on top of that process, from the SSL Certificate Checker and Vulnerability Scanner for security audits, to the Website Performance Analyzer and SEO Audit for overall site health. Head over to opdeck.co and run a free audit on your site today — you might be surprised what you find.