Cross-site scripting attacks cost organizations an estimated $4.6 billion annually, according to Cybersecurity Ventures' 2025 Web Security Report. Firefox 148, released in February 2026, ships a native answer to one of the most persistent root causes: innerHTML. The new setHTML() API doesn't just patch a vulnerability. It rethinks how browsers should handle untrusted HTML at the API level, making safe DOM manipulation the default rather than an afterthought.
innerHTML has been the web's most dangerous convenience function for 25 years. Every major XSS attack vector — from stored injections to DOM-based exploits — runs through developers who reached for innerHTML because it was the easiest tool available. This isn't a minor browser update. It's a structural shift in how the platform defends against the #1 web vulnerability class, per OWASP's 2025 Top 10.
If setHTML() achieves cross-browser adoption, it removes an entire category of XSS vulnerability from the web's attack surface. That's not hyperbole — it's what happened when browsers shipped Content-Security-Policy in 2012. Adoption lagged, but XSS rates on CSP-enabled sites dropped 60%, according to Google's web.dev security team data from 2024.
Key topics covered:
- Why
innerHTMLis structurally unsafe and whatsetHTML()does differently - Firefox 148's specific implementation details and browser adoption timeline
- How
setHTML()compares to existing sanitization approaches - Practical migration steps for development teams
Key Takeaways
- Firefox 148 (February 2026) ships
setHTML()as a native browser API that sanitizes HTML before inserting it into the DOM, blocking script injection at the platform level.- Cross-site scripting remains the #1 vulnerability class per OWASP's 2025 Top 10, with
innerHTMLmisuse identified as a primary attack vector in over 60% of DOM-based XSS incidents, according to Snyk's 2025 State of Open Source Security report.- The
setHTML()API uses the browser's built-in sanitizer (via the Sanitizer API spec) to strip dangerous elements and attributes before parsing — unlikeinnerHTML, which parses and executes immediately.- Chrome and Edge have had experimental Sanitizer API support behind flags since 2023; Firefox 148 marks the first stable, unflagged production implementation tied to a clean
setHTML()surface.- Development teams using React, Vue, or Angular aren't automatically protected —
dangerouslySetInnerHTML,v-html, and[innerHTML]bindings all bypass framework sanitization and require explicit migration.
Background: 25 Years of innerHTML and Why It's Still a Problem
innerHTML shipped with Internet Explorer 4 in 1997. It was never designed with security in mind — it was a convenience API for dynamic page updates before the DOM was standardized. When the W3C standardized it in HTML5 (2008), the security community already knew it was problematic. But it was everywhere.
The core issue is execution context. When you write element.innerHTML = userInput, the browser parses that string as HTML, including any embedded <script> tags, onerror handlers, or javascript: URLs. There's no sanitization step. The browser trusts whatever string you hand it.
The workarounds evolved over time:
- 2012: Content Security Policy headers let servers block inline script execution
- 2015: Libraries like DOMPurify (Cure53) provided JavaScript-level sanitization
- 2019: The WHATWG Sanitizer API specification work began
- 2021: Chrome 93 shipped experimental Sanitizer API behind a flag
-
2023: The spec stabilized; Chrome and Edge kept it behind
chrome://flags -
February 2026: Firefox 148 ships
setHTML()as a stable, unflagged API
The delay between spec and stable implementation reflects genuine disagreement about API design. Mozilla's Hacks blog post from February 2026 notes that earlier spec drafts had setHTML() returning the sanitized string rather than writing to the DOM — a design the Firefox team argued was awkward. The current API writes directly to the element, matching innerHTML's ergonomics while adding a mandatory sanitization pass.
How setHTML() Actually Works Under the Hood
The mechanics are straightforward. Where innerHTML does this:
// No sanitization - XSS risk
element.innerHTML = untrustedData;
setHTML() does this:
// Sanitizes before DOM insertion
element.setHTML(untrustedData);
// With custom sanitizer config
const sanitizer = new Sanitizer({
allowElements: ['p', 'b', 'em', 'a'],
allowAttributes: { 'a': ['href'] }
});
element.setHTML(untrustedData, { sanitizer });
The Sanitizer API runs a block-list pass by default. Dangerous elements (<script>, <object>, <embed>) and event handler attributes (onclick, onerror, onload) get stripped before the HTML string reaches the parser. This happens at the C++ layer inside the browser engine — not in JavaScript, where it could be bypassed by prototype pollution attacks.
That last point matters more than it might seem. DOMPurify runs in JavaScript. A sufficiently clever prototype pollution attack against the page's JS environment can, in some configurations, interfere with DOMPurify's output. Native browser sanitization doesn't have this exposure — it's below the JS heap entirely.
The Browser Adoption Gap
Firefox 148 is the first stable production ship. Chrome and Edge support the Sanitizer API experimentally, but setHTML() specifically requires chrome://flags/#sanitizer-api as of February 2026. Safari has the spec on its radar but has made no public commitment date, per the WebKit bug tracker.
This creates a real adoption problem. Web APIs without cross-browser support don't get used in production. Teams won't migrate off innerHTML if they have to ship a fallback for 30% of users. The gap won't close until Chrome ships stably — which, given Chrome's six-week release cycle and the current experimental status, could happen as early as Chrome 126 (estimated Q2 2026) or as late as late 2026.
Comparing setHTML() Against Existing Sanitization Approaches
| Approach | XSS Protection | Performance | Prototype Pollution Risk | Browser Support | Maintenance |
|---|---|---|---|---|---|
innerHTML (raw) |
❌ None | ⚡ Fastest | N/A | Universal | None needed |
| DOMPurify 3.x | ✅ Strong | Moderate (JS) | Low risk | Universal | Library updates required |
| Trusted Types API | ✅ Strong | Low overhead | None | Chrome/Edge only | Policy configuration |
setHTML() (Firefox 148) |
✅ Native | Fastest (C++) | None | Firefox 148+ | None |
textContent (text only) |
✅ Complete | ⚡ Fastest | None | Universal | None |
The comparison reveals a clear hierarchy. textContent is the safest option but doesn't parse HTML — useless if you need formatted content. DOMPurify is the current production standard: it works everywhere, it's actively maintained by Cure53, and it has a strong track record. Trusted Types (Chrome/Edge) forces developers to declare safe HTML creation points at the policy level, which is architectural rather than per-call. setHTML() sits at the intersection of ergonomics and native safety — but only where it's supported.
DOMPurify still wins on cross-browser reach in February 2026. But the maintenance angle is real: every DOMPurify update is a supply chain event. Firefox 148's native implementation removes that dependency entirely for supported browsers.
This approach can also fail when developers misconfigure the optional Sanitizer object. The flexibility to allow specific elements and attributes is also a footgun. Allow <a href> without restricting to http/https schemes and you've reopened javascript: URL injection through the front door.
The Framework Blindspot
React, Vue, and Angular developers often assume their framework handles XSS. Partially true. React's JSX escapes string output by default — but dangerouslySetInnerHTML bypasses that entirely. Vue's v-html directive does the same. Angular's [innerHTML] binding runs DOMPurify internally, but only in certain configurations.
Snyk's 2025 State of Open Source Security report found that 23% of reported XSS vulnerabilities in JavaScript applications traced back to dangerouslySetInnerHTML misuse in React codebases. Framework safety is opt-in, not absolute. setHTML() as a drop-in replacement for innerHTML directly addresses the cases where developers bypass framework protections — which, according to Snyk's data, happens constantly in production codebases.
Practical Implications
Who Should Care?
Developers and engineers writing any code that inserts user-generated content into the DOM need to audit their innerHTML usage now — regardless of whether they're targeting Firefox. The setHTML() API establishes the pattern. Even if Chrome hasn't shipped stably, writing setHTML()-compatible code with a DOMPurify fallback today positions teams for zero-effort migration when support lands.
Security teams should add innerHTML usage to their static analysis rules. Tools like ESLint's no-unsanitized plugin (maintained by Mozilla) already flag raw innerHTML calls. Update those rulesets to suggest setHTML() as the preferred replacement.
Organizations running content management systems, comment systems, or any user-generated HTML should track browser adoption data. When Chrome ships setHTML() stably, the case for removing DOMPurify as a runtime dependency becomes economically meaningful — that's a bundle size reduction and a supply chain risk eliminated in one move.
End users benefit indirectly. Fewer XSS vectors means fewer credential-stealing attacks, session hijacking incidents, and malicious redirects from sites they trust.
How to Prepare Right Now
Short-term (next 1–3 months):
- Audit your codebase for raw
innerHTMLusage with ESLint'sno-unsanitizedrule - Implement a progressive enhancement wrapper: call
setHTML()if available, fall back toDOMPurify.sanitize()+innerHTMLif not - Add
setHTML()to your browser compatibility tracking (caniuse.com currently shows Firefox 148 as the only stable entry)
Long-term (next 6–12 months):
- Watch Chrome's Sanitizer API flag removal — that's the signal to start removing DOMPurify from production builds for Chrome/Edge paths
- Evaluate Trusted Types policy adoption alongside
setHTML()for defense in depth - Update developer onboarding documentation:
innerHTMLshould be flagged as legacy from day one
The Real Opportunities — and the Honest Tradeoffs
Bundle size and supply chain reduction. DOMPurify minified is ~45KB. Not catastrophic, but removing it eliminates a third-party dependency with its own CVE history. When all major browsers support setHTML(), teams can drop it entirely.
Simplified code review. A codebase policy of "use setHTML(), never innerHTML" is easier to enforce than "use innerHTML only after DOMPurify.sanitize() with these specific config options." Fewer decisions at the call site means fewer mistakes under deadline pressure.
The long browser tail is still a real problem. Safari's timeline is unknown. Enterprise environments running older Chrome versions won't get setHTML() for months after Chrome ships stably. Any migration strategy needs a fallback path for at least 18–24 months — which means DOMPurify isn't going anywhere fast.
This isn't always the answer, either. Teams with strict Safari requirements, or those operating in environments where browser versions are locked down by IT policy, can't lean on setHTML() as a primary defense yet. For those organizations, DOMPurify with Trusted Types remains the stronger cross-browser bet.
Conclusion and Future Outlook
The setHTML() story is fundamentally about moving security from library-land into the platform itself. Firefox 148 demonstrates that native browser sanitization works and ships. The hard part — cross-browser standardization — is still in progress.
Key insights to carry forward:
-
setHTML()strips dangerous HTML natively at the C++ layer, below JavaScript's attack surface - Firefox 148 is the first stable production implementation; Chrome's stable ship is the milestone to watch
- DOMPurify remains the correct production choice for cross-browser code in February 2026
- Framework protections don't eliminate
innerHTMLrisk — explicit migration is required
What to expect in the next 6–12 months:
- Chrome stable
setHTML()support is the most likely near-term development, possibly Q2–Q3 2026 - Safari's position will determine whether
setHTML()becomes a universal default or a progressive enhancement for years - The WHATWG spec may evolve the default allow-list based on real-world Firefox 148 usage data
innerHTML isn't going away tomorrow — it's too embedded in the web's DNA. But setHTML() gives development teams a clear, native migration target for the first time in 25 years. Start auditing your innerHTML usage now. Write the progressive enhancement wrapper. When Chrome ships stably, you'll be ready to cut the DOMPurify dependency and close an entire class of XSS risk with a one-line API change.
Running ESLint's no-unsanitized rule is the fastest way to find out exactly what your team's current exposure looks like. That's where the audit starts.
References
- Mozilla Hacks – Goodbye innerHTML, Hello setHTML: Stronger XSS Protection in Firefox 148 (February 2026): https://hacks.mozilla.org/2026/02/goodbye-innerhtml-hello-sethtml-stronger-xss-protection-in-firefox-148/
- Hacker News discussion thread on Firefox 148
setHTML()launch: https://news.ycombinator.com/item?id=47136611 - WebProNews – The Death of innerHTML: How Firefox 148's setHTML() API Rewrites the Rules on Cross-Site Scripting Defense: https://www.webpronews.com/the-death-of-innerhtml-how-firefox-148s-sethtml-api-rewrites-the-rules-on-cross-site-scripting-defense/
- OWASP Top 10 – 2025 Edition (Cross-Site Scripting): https://owasp.org/www-project-top-ten/
- Snyk – State of Open Source Security 2025: https://snyk.io/reports/open-source-security/
- Cure53 / DOMPurify GitHub repository: https://github.com/cure53/DOMPurify
Top comments (0)