Coordinating SRI, CSP & Trusted Types

Permalink to "Coordinating SRI, CSP & Trusted Types"

This workflow is part of Runtime Policy Enforcement & Trusted Types, and it exists because no single browser control covers the full script attack surface. Subresource Integrity verifies that fetched bytes were not tampered with, Content Security Policy decides which sources may load and execute, and Trusted Types stops strings from reaching dangerous DOM sinks. Deploy any one of them alone and a determined attacker simply pivots to the gap the others would have closed.

The page below shows how to compose all three in a single HTTP response: which threat each layer actually stops, how require-sri-for, a nonce or hash script-src, and require-trusted-types-for coexist in one policy, the order in which to roll them out without breaking production, and the header-size and interaction pitfalls that bite teams who enable everything at once. Two focused walkthroughs go deeper on the composition itself — Combining require-sri-for with CSP and Deploying Defense-in-Depth Script Controls.

Prerequisites

Permalink to "Prerequisites"

Conceptual Foundation: Three Layers, Three Threats

Permalink to "Conceptual Foundation: Three Layers, Three Threats"

The three controls are defined by three different specifications, and each one draws its trust boundary at a different point in the resource lifecycle. Reading them together is the only way to see that they do not overlap.

  • Subresource Integrity — the W3C SRI Recommendation delegates enforcement to the browser: after a resource is fetched and any Content-Encoding is decoded, the browser hashes the bytes and blocks the resource if the digest does not match the integrity attribute. The threat it stops is a tampered fetched resource — a CDN edge that serves modified bytes, a hijacked origin, a poisoned cache. It says nothing about which resources are allowed to load, only that a resource you named must arrive byte-for-byte as expected.
  • Content Security Policy — CSP Level 3 (W3C Working Draft) governs which sources may load and execute. A script-src directive with a per-request nonce or a content hash is an allow-list: a script element runs only if it matches the policy. The threat it stops is an unauthorized source executing — an injected <script src="https://evil.example">, an inline <script> an attacker managed to write into the page, a base-tag hijack. CSP does not check whether an authorized script’s bytes were tampered with; that is SRI’s job.
  • Trusted Types — the Trusted Types specification (folded into CSP Level 3 via require-trusted-types-for) locks the DOM injection sinks. Once enforced, assigning a raw string to innerHTML, script.src, eval, or roughly sixty other sinks throws a TypeError; only a TrustedHTML, TrustedScript, or TrustedScriptURL produced by a registered policy is accepted. The threat it stops is DOM-based XSS — client-side code that takes attacker-controlled data and feeds it to a sink. Neither SRI nor a source-based CSP sees this, because the malicious markup is assembled at runtime from strings that never crossed the network as a script.

Put plainly: SRI answers “are these bytes authentic?”, CSP answers “is this source allowed?”, and Trusted Types answers “may this string become live DOM?”. An attacker who defeats one still faces the other two. The matrix below makes the coverage explicit.

Threat versus Layer Coverage Matrix A grid with four threat rows — tampered fetched bytes, unauthorized script source, inline script injection, and DOM-based XSS sink — against three layer columns: SRI, CSP source policy, and Trusted Types. Green check cells mark the layer that stops each threat; empty cells mark no coverage, showing that the three layers together cover all four rows with no single layer covering everything. SRI fetch integrity CSP source policy Trusted Types DOM sinks Tampered fetched bytes (CDN edge / cache poison) Unauthorized src loads (injected <script src>) Inline script injection (no nonce / hash) DOM-based XSS sink (innerHTML from data) No single column covers every row — coverage is complete only when all three ship together.

The specification anchor for the combined deployment is CSP Level 3, which defines both require-trusted-types-for and the script-src matching algorithm, alongside the SRI Recommendation that defines integrity. The browser evaluates them independently and in a fixed order per resource, which is why the rollout below layers them one at a time.


Step 1 — Pin Fetched Bytes with SRI

Permalink to "Step 1 — Pin Fetched Bytes with SRI"

Start here because SRI is the lowest-risk layer: it has no Report-Only mode, but it only affects the specific tags you annotate, so it cannot break a resource you have not touched. Add a sha384 digest and crossorigin="anonymous" to every cross-origin resource.

<script
  src="https://cdn.example.com/app.4f9c2a.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux8JHrklT1RNnZ3d8l6+pNnS3lPmW2"
  crossorigin="anonymous"></script>
<link
  rel="stylesheet"
  href="https://cdn.example.com/app.9a1b3c.css"
  integrity="sha384-3cskD8shRqChMKCMlMNPezEwv0GFHsT7OxGnbFPeK4mvPkVGKpkB7Vp0jLvIFRE"
  crossorigin="anonymous">

Compute each token against the exact bytes the CDN serves:

curl -sL https://cdn.example.com/app.4f9c2a.js | openssl dgst -sha384 -binary | openssl base64 -A

Expected output — a 64-character base64 string that matches the token in your markup character-for-character:

oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux8JHrklT1RNnZ3d8l6+pNnS3lPmW2

Load the page with DevTools open and confirm no net::ERR_SRI_FAILED appears. At this point tampered bytes are blocked, but nothing yet stops an injected tag that carries no integrity attribute — that is the next layer.


Step 2 — Restrict Sources with a Nonce or Hash CSP (Report-Only First)

Permalink to "Step 2 — Restrict Sources with a Nonce or Hash CSP (Report-Only First)"

Add a script-src policy that authorizes scripts by per-request nonce (or content hash), and ship it as Content-Security-Policy-Report-Only so violations are reported without blocking anything.

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0mBase64==' https://cdn.example.com;
  object-src 'none';
  base-uri 'none';
  report-to csp-endpoint

Every inline script the browser should trust carries the matching nonce, regenerated on each response:

<script nonce="r4nd0mBase64==">
  window.__APP_CONFIG__ = { env: 'production' };
</script>

Watch the report stream for a few days. Expected output at the endpoint — a report per unauthorized script, for example:

{
  "type": "csp-violation",
  "body": {
    "effectiveDirective": "script-src",
    "blockedURL": "inline",
    "disposition": "report",
    "documentURL": "https://app.example.com/checkout"
  }
}

Once the stream contains only expected entries, rename the header to Content-Security-Policy to enforce. Now an unauthorized source cannot execute even if it is injected. Nonce generation is covered in depth by the pages under Runtime Policy Enforcement & Trusted Types; the interaction between require-sri-for and this script-src is the subject of Combining require-sri-for with CSP.


Step 3 — Guard DOM Sinks with Trusted Types (Report-Only First)

Permalink to "Step 3 — Guard DOM Sinks with Trusted Types (Report-Only First)"

CSP source policy still lets first-party code build markup from strings and push it into innerHTML. Trusted Types closes that. Deploy require-trusted-types-for 'script' in Report-Only and enumerate the sinks your code hits.

Content-Security-Policy-Report-Only:
  require-trusted-types-for 'script';
  trusted-types default;
  report-to csp-endpoint

Expected output — a report for each unguarded sink assignment:

{
  "type": "csp-violation",
  "body": {
    "effectiveDirective": "require-trusted-types-for",
    "sample": "Element innerHTML|<div>${userBio}</div>",
    "disposition": "report"
  }
}

For each reported sink, route the value through a registered policy instead of assigning a raw string:

// Register once, at startup, before any sink is touched
const sanitizer = trustedTypes.createPolicy('default', {
  createHTML: (input) => DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: false }),
});

// At the call site, the sink now receives a TrustedHTML
element.innerHTML = sanitizer.createHTML(userSuppliedMarkup);

When the report stream is clean, promote to the enforcing Content-Security-Policy header. A raw-string assignment to a sink now throws TypeError: Failed to set the 'innerHTML' property … This document requires 'TrustedHTML' assignment.


Step 4 — Compose the Directives in One Response

Permalink to "Step 4 — Compose the Directives in One Response"

With each layer validated in isolation, merge the CSP directives into a single enforcing header. SRI stays in the markup; everything else is one Content-Security-Policy value.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0mBase64==' https://cdn.example.com;
  style-src 'self' https://cdn.example.com;
  require-trusted-types-for 'script';
  trusted-types default;
  object-src 'none';
  base-uri 'none';
  report-to csp-endpoint
<!-- Same response, markup layer: SRI still pins the fetched bytes -->
<script
  src="https://cdn.example.com/app.4f9c2a.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux8JHrklT1RNnZ3d8l6+pNnS3lPmW2"
  crossorigin="anonymous"></script>

Expected output — three independent verdicts per script load, observable in DevTools:

1. CSP script-src: nonce/host matches            → source authorized
2. SRI: sha384 digest of decoded bytes matches   → bytes authentic
3. Trusted Types: no raw-string sink assignment  → no DOM XSS violation

All three must pass for the script to run and for the code it contains to touch the DOM. The full worked response — nonce CSP plus require-trusted-types-for plus an SRI <script> — is assembled step by step in Deploying Defense-in-Depth Script Controls.


Step 5 — Verify Coverage and Watch Header Size

Permalink to "Step 5 — Verify Coverage and Watch Header Size"

Prove each layer fires on its own threat, and guard the two pitfalls that break combined deployments: header truncation and directive interaction.

# 1. Confirm the composed header is present and under the proxy ceiling
curl -sI https://app.example.com/ | awk '
  /^[Cc]ontent-[Ss]ecurity-[Pp]olicy:/ { n = length($0); print "CSP header bytes:", n;
    if (n > 8192) print "WARNING: exceeds 8 KB proxy ceiling" }'

Expected output:

CSP header bytes: 412
# 2. Confirm SRI, CSP source, and Trusted Types each block their own threat
#    (drive these in an automated browser test; watch console + report endpoint)
#    - swap one CDN byte      → expect net::ERR_SRI_FAILED
#    - inject a nonce-less tag → expect "Refused to load ... violates ... script-src"
#    - assign string to innerHTML → expect "requires 'TrustedHTML' assignment"

Gate deployment on the violation report rate exactly as you would for SRI alone: if the combined endpoint sees a spike after a release, block promotion and roll back. Keep the serialized policy compact — prefer nonces over long hash lists, drop directives that duplicate default-src, and never inline a hash list that grows with every build.


Configuration Reference

Permalink to "Configuration Reference"

The table below is the single source of truth for which layer owns which threat and which directive expresses it. Map every control you deploy back to a row.

Threat Layer Directive / attribute Enforcing value
Tampered fetched bytes (CDN edge, cache poison, hijacked origin) SRI integrity + crossorigin integrity="sha384-…" crossorigin="anonymous"
Fetched resource lacks any integrity check SRI via CSP require-sri-for require-sri-for script style (limited support — see gotchas)
Unauthorized script source loads CSP script-src 'self' 'nonce-…' https://cdn.example.com
Inline script injected without a nonce CSP script-src nonce/hash 'nonce-<per-request>' or 'sha384-<hash>'
<base> hijack redirecting relative script URLs CSP base-uri 'none'
Plugin / object injection CSP object-src 'none'
DOM-based XSS via string sink Trusted Types require-trusted-types-for 'script'
Rogue Trusted Types policy creation Trusted Types trusted-types default (or a named allow-list)
Missing telemetry on any violation Reporting report-to report-to csp-endpoint

require-sri-for warrants its own caveat: it shipped behind a flag and was subsequently removed from Chromium, so it cannot be your enforcement mechanism today. Treat it as belt-and-suspenders where supported and rely on build-time enforcement plus the source policy — the full story is in Combining require-sri-for with CSP.


Integration with Adjacent Tooling

Permalink to "Integration with Adjacent Tooling"

Coordinating these three controls sits at the junction of the fetch-integrity work and the runtime-policy work, so it feeds forward and backward into several adjacent workflows:

  • Configuring Content Security Policy with SRI — the upstream reference for the CSP-and-SRI half of the composition, including script-src hash syntax and how the two headers are parsed.
  • Browser Enforcement & Security Boundaries — documents the exact point in the fetch pipeline where the SRI gate fires, which determines the order in which the three layers evaluate.
  • CI/CD nonce and hash injection — per-request nonces must be generated server-side or at the edge on every response; a static nonce is equivalent to unsafe-inline. Wire nonce generation into the same template layer that reads your SRI manifest so both are produced from one build artifact.
  • Reporting pipeline — a single report-to endpoint receives CSP source violations, require-trusted-types-for violations, and (where supported) SRI-related reports. Tag each by effectiveDirective so your dashboards attribute a spike to the correct layer.

Troubleshooting

Permalink to "Troubleshooting"

Refused to execute inline script because it violates the following Content Security Policy directive: script-src

The inline script’s nonce does not match the header, or the nonce is static across responses. Confirm the server injects a fresh, cryptographically random nonce into both the header and every trusted inline <script> on the same response. A cached HTML page with a stale nonce produces this on every subsequent load.

net::ERR_SRI_FAILED appears only after enabling CSP

CSP did not cause the mismatch — enabling it merely surfaced a script that was already failing SRI but previously loaded from cache. Re-derive the digest from the live CDN bytes with openssl dgst -sha384 -binary. A common trigger is a CDN transformation (minification, edge injection) that mutates the payload after your build computed the hash.

Uncaught TypeError: Failed to set the 'innerHTML' property … This document requires 'TrustedHTML' assignment

Trusted Types enforcement is live but a code path assigns a raw string to a sink without going through a registered policy. Find the call site from the report sample field, wrap the value in your createHTML policy, and redeploy. If a third-party library triggers it, register a default policy so its assignments are sanitized centrally rather than editing vendor code.

CSP header silently missing in production but present in staging

A proxy, WAF, or CDN truncated the response headers because the serialized policy exceeded a size limit (commonly 8 KB, sometimes 4 KB). Measure the header length, replace inline hash lists with a single per-request nonce, and remove redundant directives that repeat default-src.

Trusted Types breaks a framework that writes to the DOM (React, Angular, lit)

The framework uses a sink internally. Modern frameworks ship Trusted Types compatibility, but a custom directive or an old version may not. Register a named policy the framework can use, add its policy name to the trusted-types allow-list, and confirm in Report-Only before enforcing.

Two Content-Security-Policy headers are sent and the stricter one always wins unexpectedly

The browser enforces all CSP headers, taking the intersection. If middleware appends a second policy, a directive you thought you set to 'self' is being narrowed by the other header. Emit exactly one composed policy, or audit every layer (framework, reverse proxy, CDN) that may add its own.


Security Boundary

Permalink to "Security Boundary"

Coordinating SRI, CSP, and Trusted Types hardens the script and DOM attack surface, but the combined stack still has a defined edge. Together these controls do not:

  • Vouch for the build that produced the bytes. SRI pins whatever your pipeline emitted; if a compromised CI job built a malicious bundle, every layer here validates it happily. Build provenance is a separate concern — see Provenance Verification Workflows.
  • Vet the dependencies that entered the bundle. A malicious transitive package is inside the hashed artifact. Dependency-level assurance requires Dependency Pinning Best Practices, not runtime policy.
  • Protect users on browsers that ignore a directive. require-trusted-types-for and nonce-based CSP degrade to no-ops on engines that do not implement them; SRI on those engines still works, which is another reason the layers are complementary rather than substitutable.
  • Stop a network attacker who holds a valid TLS certificate and serves unmodified bytes. SRI detects byte changes, not origin substitution that returns the expected content.
  • Recover a blocked resource. When a layer blocks, the resource is simply unavailable; graceful degradation is handled separately in Handling SRI Failures with onerror Handlers.

Frequently asked questions

Permalink to "Frequently asked questions"
Do SRI, CSP, and Trusted Types overlap or duplicate protection?

They do not overlap. SRI verifies the bytes of a fetched resource, CSP decides which sources may load or execute, and Trusted Types guards the DOM sinks that turn strings into executable markup. A payload that clears one layer is still stopped by another — an injected tag with valid bytes is blocked by CSP, a policy-authorized script whose CDN bytes were swapped is blocked by SRI, and a runtime innerHTML assignment that never crossed the network is blocked by Trusted Types.

Which layer should I deploy first?

Deploy SRI first: it has no Report-Only mode but the lowest breakage risk because it only affects tags you explicitly annotate. Add a nonce or hash script-src CSP second, in Report-Only, then Trusted Types third, also in Report-Only. Enforce each layer only after its violation stream is quiet. The numbered steps above follow exactly this order.

Can all three controls live in one HTTP response?

Yes. SRI lives in the integrity attributes of your markup, while script-src, require-trusted-types-for, and trusted-types are directives inside a single Content-Security-Policy header. The only practical constraint is header size — keep the serialized policy under the 8 KB ceiling most proxies impose, favouring per-request nonces over long inline hash lists.

Does a nonce-based CSP make SRI redundant?

No. A nonce authorizes a specific script element to run, but it says nothing about whether the fetched bytes were tampered with in transit or at the CDN edge. SRI is the only one of the three that verifies content integrity. Nonces authorize; SRI verifies — you want both on a cross-origin <script>.

What happens if I enforce Trusted Types before auditing my sinks?

Every unguarded assignment to innerHTML, script.src via string, eval, and similar sinks throws a TypeError and the feature breaks. Always run require-trusted-types-for 'script' in Report-Only first, enumerate the violated sinks from the reports, wrap each one in a registered policy, and only then switch to the enforcing header.

How do I keep the composed CSP header from getting too large?

Prefer one per-request nonce over a growing list of 'sha384-…' inline hashes, collapse directives that merely restate default-src, and move any long allow-list into a tighter set of origins. Measure the header on every deploy and fail the build if it approaches the proxy limit, because a truncated header silently disables all three policy layers at once.


Permalink to "Related"

Articles in This Topic

Combining require-sri-for with CSP
Deploying Defense-in-Depth Script Controls
Back to Runtime Policy Enforcement & Trusted Types