opdeck / blog / enhancing-web-security-xss-protection

How to Use setHTML for Stronger XSS Protection in Firefox 148

July 3, 2026 / OpDeck Team
XSS ProtectionFirefox 148Web SecuritySanitizer APIsetHTML

Cross-site scripting has been a thorn in the side of web developers for decades. Despite being well-understood and extensively documented, XSS consistently ranks among the top vulnerabilities in web applications year after year. The arrival of the Sanitizer API in Firefox 148, with its new setHTML method, marks a meaningful shift in how browsers help developers write safer code by default. But browser-level protection is only one piece of the puzzle. Building a genuinely secure web application requires a layered approach that spans your HTML sanitization strategy, your HTTP headers, your SSL configuration, and even your SEO hygiene.

This guide breaks down what the Sanitizer API actually does, how to use it properly, and how to fit it into a broader security and performance strategy.


What Is XSS and Why Does It Keep Happening?

Cross-site scripting occurs when an attacker injects malicious scripts into content that gets served to other users. The browser, unable to distinguish between legitimate page scripts and injected ones, executes both. The results range from session hijacking and credential theft to full account takeover.

The root cause is almost always the same: a developer takes untrusted input — from a URL parameter, a form submission, a database record, or a third-party API — and inserts it directly into the DOM without proper sanitization. The classic offender is innerHTML:

// Dangerous: do not do this
document.getElementById('output').innerHTML = userInput;

If userInput contains something like <img src=x onerror="stealCookies()">, the browser will happily execute that payload. Developers have historically relied on third-party libraries like DOMPurify to sanitize HTML before this kind of insertion. Those libraries work well, but they add dependency weight, require regular updates, and are easy to forget to apply consistently.


The Sanitizer API and setHTML: A Native Solution

The Sanitizer API is a browser-native mechanism that lets you sanitize untrusted HTML strings before they reach the DOM. Firefox 148 is the first browser to ship the standardized version of this API, and it introduces the setHTML method as the primary interface.

How setHTML Works

Instead of setting innerHTML directly, you use setHTML, which accepts an HTML string and automatically sanitizes it before insertion:

// Safe: sanitized before DOM insertion
const element = document.getElementById('output');
element.setHTML(userInput);

By default, setHTML strips out script elements, event handler attributes (onclick, onerror, etc.), and other known vectors for script injection. You do not need to configure anything to get baseline protection — the safe behavior is the default.

Customizing the Sanitizer

For more control, you can instantiate a Sanitizer object with a custom configuration and pass it to setHTML:

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

element.setHTML(userInput, { sanitizer });

This approach is useful when you want to permit a specific subset of HTML — for example, in a rich text editor output area where you want to allow bold and italic formatting but nothing else. The explicit allowlist model is far safer than trying to maintain a denylist of dangerous tags.

setHTMLUnsafe for Trusted Content

The API also includes setHTMLUnsafe, which skips sanitization entirely. This is intended for cases where you are inserting HTML that you have already verified through other means, or where you are working with declarative shadow DOM markup. The name is intentionally alarming to discourage casual use:

// Only use this when you fully control the content
element.setHTMLUnsafe(trustedServerRenderedHTML);

Migrating from innerHTML to setHTML

Migrating an existing codebase from innerHTML to setHTML is straightforward in most cases, but it requires a systematic audit rather than a find-and-replace operation.

Step 1: Identify All innerHTML Assignments

Search your codebase for every assignment to innerHTML. Not all of them are dangerous — if you are setting innerHTML with a hardcoded string literal, there is no user input involved. Focus on cases where the right-hand side comes from a variable:

# Quick grep to find candidates
grep -rn "innerHTML\s*=" ./src --include="*.js" --include="*.ts"

Step 2: Classify Each Usage

For each innerHTML assignment, determine the source of the data:

  • Hardcoded string: Low risk, but consider switching to textContent or setHTML for consistency
  • Server-rendered data you control: Moderate risk, depends on whether that data can be influenced by user input upstream
  • User-supplied input: High risk, must be sanitized

Step 3: Replace with setHTML or textContent

If the content is plain text with no HTML formatting intended, switch to textContent — it never interprets HTML at all:

// No HTML needed? Use textContent
element.textContent = userInput;

If the content is expected to contain HTML formatting, switch to setHTML:

// HTML formatting needed? Use setHTML
element.setHTML(userInput);

Step 4: Handle Browser Compatibility

The Sanitizer API is currently in Firefox 148+ but not yet in all browsers. You need a fallback strategy. DOMPurify remains the most reliable cross-browser option:

function safeSetHTML(element, html) {
  if (typeof element.setHTML === 'function') {
    element.setHTML(html);
  } else {
    // Fallback: DOMPurify for browsers without native Sanitizer API
    element.innerHTML = DOMPurify.sanitize(html);
  }
}

This progressive enhancement approach lets you take advantage of the native API where available while maintaining safety in older browsers.


XSS Is Not Just a JavaScript Problem

Fixing innerHTML usage is important, but XSS can enter your application through multiple vectors. A complete defense requires attention to your HTTP response headers, your Content Security Policy, and your server-side rendering logic.

Content Security Policy

A Content Security Policy (CSP) is one of the most effective mitigations against XSS because it restricts which scripts the browser is allowed to execute, regardless of how they ended up in the page:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'; object-src 'none'; base-uri 'self';

A strict CSP using nonces or hashes prevents inline script execution entirely, which means even a successful XSS injection cannot execute — the browser blocks it. This is defense in depth: even if your sanitization fails, the CSP catches it.

Security Headers Beyond CSP

Several other HTTP response headers contribute to XSS defense:

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: nosniff prevents MIME-type sniffing attacks, which can be used to execute HTML files as scripts. X-Frame-Options: DENY prevents clickjacking, which is often used in conjunction with XSS.

You can audit your current security headers using the Vulnerability Scanner on OpDeck, which checks for missing or misconfigured security headers including CSP, HSTS, and X-Frame-Options.


SSL/TLS: The Foundation That Makes Everything Else Matter

None of your XSS protections matter much if your traffic is not encrypted. An attacker performing a man-in-the-middle attack on an HTTP connection can inject scripts directly into your HTML responses before they reach the user's browser — bypassing all your client-side sanitization entirely.

Why HTTPS Is Non-Negotiable for Security

When your site runs over HTTPS, the connection between the server and the browser is encrypted and authenticated. The browser verifies that the SSL certificate is valid and issued for your domain, which means an attacker cannot silently modify your HTML in transit.

Beyond encryption, HTTPS is required for several modern browser security features:

  • Secure cookies: The Secure flag on cookies only works over HTTPS
  • HSTS: HTTP Strict Transport Security forces future connections to use HTTPS
  • Service Workers: Required for PWA functionality and offline caching
  • Mixed content blocking: Browsers block HTTP subresources on HTTPS pages

Auditing Your SSL Configuration

Having a certificate is not enough. Misconfigured SSL — expired certificates, weak cipher suites, missing HSTS headers, or certificate chain issues — creates vulnerabilities even on sites that technically "use HTTPS."

The SSL Certificate Checker on OpDeck gives you a quick read on your certificate's validity, expiration date, issuer chain, and whether HSTS is properly configured. It is worth running this check periodically, not just at initial deployment, because certificates expire and configurations drift.

Key things to verify in your SSL setup:

✓ Certificate is valid and not expired
✓ Certificate chain is complete (no missing intermediates)
✓ TLS 1.2 and 1.3 are supported; TLS 1.0 and 1.1 are disabled
✓ HSTS header is present with a max-age of at least 1 year
✓ HSTS includes subdomains if applicable
✓ Certificate covers all subdomains you serve (wildcard or SAN)

HSTS Preloading

For maximum protection, submit your domain to the HSTS preload list. This tells browsers to always use HTTPS for your domain, even on the very first visit, before any HTTP response has been received:

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

Be cautious with includeSubDomains — it applies to every subdomain, including ones you may not have HTTPS configured on yet.


SEO and Security: More Connected Than You Think

There is a meaningful relationship between your site's security posture and its search engine performance that often gets overlooked. Google has used HTTPS as a ranking signal since 2014, and more recently, Core Web Vitals — which include metrics related to page stability and load performance — factor into rankings.

How Security Issues Affect SEO

A compromised site that serves malware or spam links will be flagged by Google's Safe Browsing system and removed from search results. Even less severe issues create problems:

  • Mixed content warnings cause browsers to block subresources, breaking page functionality and potentially triggering security warnings that drive users away
  • Malicious redirects injected via XSS can redirect users to spam sites, which Google interprets as the site itself being untrustworthy
  • Injected hidden links are a common goal of XSS attacks — attackers inject invisible links to boost their own SEO rankings, which can result in your site being penalized for unnatural link patterns

Running a Security-Aware SEO Audit

A regular SEO audit should include checks that touch on security: are your canonical tags intact, are your meta descriptions free of injected content, are your structured data markup and Open Graph tags rendering correctly?

The SEO Audit tool on OpDeck checks meta tags, heading structure, canonical URLs, and content signals — giving you a baseline to compare against after any security incident. If something has been injected into your pages, it often shows up as unexpected changes in meta descriptions or title tags that an audit will surface.

Structured Data and XSS

JSON-LD structured data blocks are a less obvious XSS vector that deserves attention. If you dynamically generate JSON-LD using user-supplied data and inject it into a <script type="application/ld+json"> block without proper escaping, you can create an XSS vulnerability:

// Dangerous: user data injected into script block
const jsonLd = `<script type="application/ld+json">
  {"name": "${userInput}"}
</script>`;

If userInput contains </script><script>alert(1), the browser will close the JSON-LD script block and open a new executable one. Always use JSON.stringify to serialize user data into JSON-LD:

// Safe: JSON.stringify handles escaping
const data = {
  "@context": "https://schema.org",
  "@type": "Product",
  "name": userInput // JSON.stringify escapes special characters
};
const jsonLd = `<script type="application/ld+json">
  ${JSON.stringify(data)}
</script>`;

Building a Layered Security Checklist

Security is not a single feature you add — it is a set of overlapping practices that each catch what the others miss. Here is a practical checklist that combines the techniques covered in this article:

Input Handling

  • Replace innerHTML assignments with setHTML (or textContent for plain text)
  • Add DOMPurify as a fallback for browsers without the Sanitizer API
  • Validate and sanitize all user input server-side before storing it
  • Use JSON.stringify for any user data inserted into script blocks

HTTP Headers

  • Implement a Content Security Policy with nonces or hashes
  • Set X-Content-Type-Options: nosniff
  • Set X-Frame-Options: DENY or use CSP frame-ancestors
  • Configure HSTS with a long max-age

SSL/TLS

  • Verify certificate validity and chain completeness
  • Disable TLS 1.0 and 1.1
  • Enable HSTS and consider preload submission
  • Check for mixed content issues

Monitoring

  • Run periodic SEO audits to detect injected content
  • Monitor SSL certificate expiration
  • Scan security headers regularly
  • Review Content Security Policy violation reports

Practical Testing Workflow

Once you have implemented these changes, test them systematically rather than assuming they work:

// Test your setHTML implementation with known XSS payloads
const testPayloads = [
  '<script>alert("xss")</script>',
  '<img src=x onerror="alert(1)">',
  '<svg onload="alert(1)">',
  '<a href="javascript:alert(1)">click</a>',
  '"><script>alert(1)</script>',
];

const testElement = document.createElement('div');
testPayloads.forEach(payload => {
  testElement.setHTML(payload);
  console.log('Sanitized:', testElement.innerHTML);
  // Verify no script tags or event handlers remain
});

For your HTTP headers, tools like curl give you a quick view of what your server is actually sending:

curl -I https://yourdomain.com | grep -i "content-security\|strict-transport\|x-content\|x-frame"

Conclusion

The Sanitizer API and setHTML represent a genuine improvement in the browser's ability to help developers write safer code. Making the safe behavior the default — rather than requiring developers to opt into sanitization — is exactly the right approach. But browser APIs are one layer in a defense-in-depth strategy that also requires proper SSL configuration, well-tuned HTTP security headers, and ongoing monitoring for signs of compromise.

If you want to quickly assess where your site stands across these dimensions, OpDeck provides a suite of tools purpose-built for this kind of audit. The SSL Certificate Checker will surface certificate and HSTS issues, the SEO Audit will help you detect unexpected changes to your page content, and the Vulnerability Scanner will check your security headers against current best practices. Run them together and you get a fast, practical picture of your site's security and performance posture — a solid starting point for any hardening effort.