DEV Community

Cover image for Firefox 148's setHTML() API: An innerHTML Replacement for XSS Protection
Maverick-jkp
Maverick-jkp

Posted on • Originally published at jakeinsight.com

Firefox 148's setHTML() API: An innerHTML Replacement for XSS Protection

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 innerHTML is structurally unsafe and what setHTML() 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 innerHTML misuse 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 — unlike innerHTML, 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;
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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 innerHTML usage with ESLint's no-unsanitized rule
  • Implement a progressive enhancement wrapper: call setHTML() if available, fall back to DOMPurify.sanitize() + innerHTML if 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: innerHTML should 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 innerHTML risk — 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

  1. 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/
  2. Hacker News discussion thread on Firefox 148 setHTML() launch: https://news.ycombinator.com/item?id=47136611
  3. 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/
  4. OWASP Top 10 – 2025 Edition (Cross-Site Scripting): https://owasp.org/www-project-top-ten/
  5. Snyk – State of Open Source Security 2025: https://snyk.io/reports/open-source-security/
  6. Cure53 / DOMPurify GitHub repository: https://github.com/cure53/DOMPurify

Top comments (0)