Browser Security Policies
When to Use
Apply when hardening an existing web application incrementally against client-side attacks. Section 12.0 covers which security headers to set; this section covers the deployment workflow for safely enforcing CSP, protecting DOM sinks with Trusted Types, enabling cross-origin isolation, verifying request origin with Fetch Metadata, and wiping browser state at logout.
Decision
| If you need to... | Use... | Why |
|---|---|---|
| Roll out CSP without breaking the app | Report-Only → Analyze → Enforce workflow | Strict CSP applied blindly breaks inline scripts silently for users |
| Block DOM-sink XSS at runtime | Trusted Types (require-trusted-types-for 'script') |
Enforces safe DOM writes even when XSS payloads bypass other defenses |
| Enable SharedArrayBuffer or WebAssembly multi-threading | Cross-origin isolation (COOP + COEP) | Browsers require crossOriginIsolated = true for these APIs |
| Reject suspicious cross-site subresource requests server-side | Fetch Metadata resource-isolation policy | Sec-Fetch-* headers cannot be forged by cross-site scripts |
| Wipe all browser state on logout | Clear-Site-Data response header |
Clears cookies, localStorage, sessionStorage, and cache in one response |
Pattern
CSP — Phase 1: Report-Only discovery (run for days to weeks):
Reporting-Endpoints: csp-endpoint="https://reports.example/csp"
Content-Security-Policy-Report-Only: script-src 'nonce-{RANDOM}' 'strict-dynamic' 'report-sample'; object-src 'none'; base-uri 'none'; report-to csp-endpoint;
'report-sample' includes the first 40 characters of violating code in each report. The 'strict-dynamic', https:, and 'unsafe-inline' tokens form a backwards-compatibility ladder: modern browsers honor 'strict-dynamic' and ignore the others; older browsers fall back to https: then 'unsafe-inline'.
Phase 2 — Analyze reports:
| Violation pattern | Action |
|---|---|
| Many inline-script violations | Add server-rendered nonces to <script> tags; use hashes for static/cached HTML (SPAs) |
| Third-party analytics violations | Use 'strict-dynamic' with a nonce so the loader propagates trust to its children. Never add the analytics domain to a URL allowlist |
| Trusted Types violations on specific sinks | Refactor those sinks before enforcing |
Phase 3 — Enforce (only when violations near zero):
Reporting-Endpoints: csp-endpoint="https://reports.example/csp"
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic' 'report-sample'; object-src 'none'; base-uri 'none'; report-to csp-endpoint;
Keep report-to on the enforced header so regressions remain visible.
Trusted Types — incremental rollout:
// 1. Audit sinks via report-only first
// 2. Refactor sinks to use safe DOM APIs (textContent, createElement)
// 3. For unavoidable sinks, route through a named policy:
if (window.trustedTypes && trustedTypes.createPolicy) {
const policy = trustedTypes.createPolicy('default', {
createHTML: (input) => DOMPurify.sanitize(input)
});
el.innerHTML = policy.createHTML(untrustedContent);
}
// 4. Move to global enforcement only after violations drop to zero
Cross-origin isolation (only when SharedArrayBuffer or WASM threading is needed):
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Every embedded subresource must serve Cross-Origin-Resource-Policy: cross-origin or the browser blocks it. Use Report-Only headers to discover breakage before enforcing. Note: COOP: same-origin severs window.opener references — audit OAuth popups and payment gateway flows.
Chromium-only alternative (Chrome 142+):
Document-Isolation-Policy: isolate-and-credentialless
Strips credentials from non-CORS cross-origin requests instead of blocking them. Not implemented in other browsers.
Fetch Metadata resource-isolation middleware:
app.use((req, res, next) => {
const site = req.get('Sec-Fetch-Site');
if (!site) return next(); // Legacy browsers: fail open
if (['same-origin', 'same-site', 'none'].includes(site)) return next();
// Allow top-level navigation GET requests (ordinary link clicks)
const mode = req.get('Sec-Fetch-Mode');
const dest = req.get('Sec-Fetch-Dest');
if (site === 'cross-site' && mode === 'navigate' &&
req.method === 'GET' && !['object', 'embed'].includes(dest)) {
return next();
}
return res.status(403).send('Forbidden');
});
Prevent CDNs from caching a blocked response: Vary: Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site
Clear-Site-Data on logout (server-side response header):
Clear-Site-Data: "cookies", "storage", "cache"
Clears cookies, localStorage, sessionStorage, and HTTP cache in one response. All modern browsers support it.
Common Mistakes
- Deploying enforced CSP without report-only first → Strict policies silently break inline scripts for real users; run report-only for days to weeks
- URL allowlists in CSP →
script-src https://cdn.example.comis bypassed via open redirects or JSONP on the allowlisted domain; use nonces or hashes - Trusted Types without framework and widget support audit → Third-party widgets writing raw strings to DOM sinks will throw at assignment time
- COEP without CORP on every embedded subresource → A single third-party image lacking
Cross-Origin-Resource-Policyblocks the entire page - Missing
Varyheader on Fetch Metadata-protected endpoints → CDNs may cache a 403 and serve it to legitimate users - Only deleting the session cookie on logout → localStorage, sessionStorage, and cached responses persist;
Clear-Site-Datacloses all state at once
See Also
- Security Headers — 12.0 which headers to set and recommended values
- XSS Prevention Patterns — CSP basics, nonce generation, DOMPurify
- Cross-Site Scripting (XSS) — safe DOM APIs (textContent vs innerHTML)
- Reference: W3C Trusted Types specification
- Reference: MDN Cross-Origin-Embedder-Policy
- Reference: Fetch Metadata — web.dev
- Reference: MDN Clear-Site-Data