Unified Script Policy Architecture

Permalink to "Unified Script Policy Architecture"

This page sits inside Runtime Policy Enforcement & Trusted Types and is the one that joins the pieces together. Most teams arrive at script security by accretion: someone adds integrity attributes to the two CDN tags in the page head, someone else ships a Content Security Policy that was copied from a blog post and softened until the site stopped breaking, and a third person enables Trusted Types on a single new route because a penetration test flagged an innerHTML sink. Each change is defensible on its own. Together they produce a policy with overlapping coverage in the places nobody was attacking and a clean gap in the place they were.

The gap exists because the three mechanisms answer three genuinely different questions, and it is easy to mistake one answer for another. Subresource Integrity answers are these the bytes I approved? Content Security Policy answers is this thing allowed to become a script at all? Trusted Types answers may this string be written into a DOM sink that turns strings into code? A policy that is strong on one axis and silent on the others is not defence in depth; it is one control with two decorations. This page treats the three as a single design, states the guarantee and the blind spot of each layer precisely, works through the interaction rules that trip people up, gives a reference policy for a typical application with a build step, sets a rollout order that does not break production, and closes with the telemetry and the compliance mapping that prove the policy is doing its job.

Prerequisites

Permalink to "Prerequisites"

Conceptual Foundation: Three Questions, Three Specifications

Permalink to "Conceptual Foundation: Three Questions, Three Specifications"

The three layers are defined by three separate specifications, and the separation is not accidental — each was written to close a hole the others were never designed to see.

Subresource Integrity (W3C, Subresource Integrity) adds an integrity attribute carrying one or more base64-encoded digests prefixed with an algorithm token. When the response arrives, the browser computes the digest of the decoded body and blocks the resource unless one of the listed digests matches. It applies to <script> and to <link> elements with a relationship the specification recognises, including stylesheet, preload and modulepreload. It says nothing about whether the URL was a sensible one to request. If an attacker with write access to your HTML adds a script tag pointing at their own file and includes a correct digest for it, SRI will happily verify the bytes and let it run.

Content Security Policy (W3C, Content Security Policy Level 3) is the layer that decides whether a resource may become a script. Its script-src directive — refined by script-src-elem for elements and script-src-attr for inline event handlers — takes a list of sources: origins, schemes, the 'nonce-…' and 'sha256-…' expressions, and the 'strict-dynamic' keyword. The nonce and hash forms are what make a policy meaningfully strict, and the details of choosing between them belong to CSP Nonces & Hash-Based Policies. What CSP never inspects is the content of a script it has already authorised. A nonce-bearing tag pointing at a compromised CDN file passes CSP without complaint.

Trusted Types (W3C Web Application Security Working Group draft) works at a completely different place in the pipeline: the assignment, not the fetch. With require-trusted-types-for 'script' in force, the browser refuses plain strings at the DOM sinks that convert strings into markup or code — innerHTML, outerHTML, document.write, script.src, eval and their relatives — and accepts only TrustedHTML, TrustedScript or TrustedScriptURL objects minted by a registered policy. This structurally removes the DOM XSS class that neither of the other layers can perceive, because in a DOM XSS the attacker never fetches anything; the injection happens inside a script that CSP already trusts and whose bytes SRI already verified. The mechanics of writing and naming those policies live in Trusted Types & DOM XSS Prevention.

Drawn along the path of a single page load, the three checkpoints sit at three different moments, and two of them are on branches the other never touches.

Three checkpoints in one script load A two-lane flow diagram. The upper lane shows a page requesting a script, a Content Security Policy check deciding whether it may load, a Subresource Integrity check on the returned bytes, and the script executing. A connector leads from execution down to the lower lane, where a running script writes to a DOM sink, a Trusted Types check requires a typed value, a policy runs a sanitizer, and the DOM is updated or a TypeError is thrown. One script load, three checkpoints network path above, DOM sink path below Page requests a script CSP script-src may it load? SRI integrity are bytes right? Script executes in page context at runtime Script writes to a DOM sink Trusted Types typed value? Policy runs the sanitizer DOM updated or TypeError SRI never sees the lower lane; Trusted Types never sees the upper one.

Layer Boundaries and Blind Spots

Permalink to "Layer Boundaries and Blind Spots"

Before any configuration, write down what each layer is for. The table below is the version worth pinning to a wall, because almost every argument about script policy comes from someone assuming a layer covers a column it does not.

Layer Protects against Blind spot Header or attribute
Subresource Integrity A CDN, mirror or edge transform serving different bytes at the same URL; cache poisoning of a static asset Whether the request should have been made; anything that happens after the approved bytes execute integrity="sha384-…" plus crossorigin="anonymous" on <script> and <link>
CSP script-src with nonces Injected <script src> tags, injected inline scripts, javascript: URLs, tags added by a compromised template The content of a script it has already allowed; the behaviour of first-party code Content-Security-Policy: script-src 'nonce-…' 'strict-dynamic'
CSP 'strict-dynamic' Host allow-list bypasses via JSONP endpoints and open redirects on allowed origins Anything a trusted script chooses to load, since trust propagates by design 'strict-dynamic' inside script-src
Trusted Types DOM XSS through innerHTML, document.write, script.src and eval in first-party and library code Network-level substitution; scripts that never touch a string sink require-trusted-types-for 'script'; trusted-types <names>
CSP reporting Silence — a policy nobody is measuring Violations in browsers that ignore the reporting directive you chose report-to, report-uri, Reporting-Endpoints

Read the blind-spot column as an attack brief. An attacker who can modify your HTML template defeats SRI trivially, because they write both the tag and the digest. An attacker who compromises a CDN defeats CSP trivially, because the origin is allow-listed and the nonce is on the tag your own server wrote. An attacker who finds a reflected parameter that reaches element.innerHTML defeats both, because no fetch occurs at all. Only the union of the three closes all three doors.

Step 1 — Establish the CSP Backbone

Permalink to "Step 1 — Establish the CSP Backbone"

Start with CSP because it is the layer the other two hang from. The target shape is a nonce-based policy with 'strict-dynamic', which is the only form that survives contact with real applications that load scripts at runtime. Generate 128 bits of randomness per response, never per session and never at build time.

// server/csp.js — Express middleware issuing a per-response nonce
import crypto from 'node:crypto';

export function scriptPolicy(req, res, next) {
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.nonce = nonce;

  res.setHeader('Reporting-Endpoints', 'csp-endpoint="https://reports.example.com/csp"');
  res.setHeader('Content-Security-Policy', [
    "base-uri 'none'",
    "object-src 'none'",
    `script-src 'nonce-${nonce}' 'strict-dynamic' 'unsafe-inline' https:`,
    "require-trusted-types-for 'script'",
    'trusted-types default app-loader',
    "frame-ancestors 'none'",
    'report-uri /csp-report',
    'report-to csp-endpoint'
  ].join('; '));

  next();
}

Two entries in that script-src look wrong and are not. 'unsafe-inline' and https: are ignored by every browser that understands 'strict-dynamic', and they exist purely so that an engine which does not understand nonces or 'strict-dynamic' still gets a working page rather than a blank one. report-uri is formally deprecated but remains the only reporting directive some engines honour, so send both.

Verification signal. Fetch the page twice and confirm the nonce differs, then confirm the template consumed it:

for i in 1 2; do
  curl -sI https://app.example.com/checkout | grep -io "nonce-[A-Za-z0-9+/=]*"
done
# nonce-9Yd0Qm2p1s7Kx4Vb8Lz3Ug==
# nonce-Tf5Rb1Nq7Jc2Mw9Ph6Xy4A==

If the two values match, the nonce is being cached — either by your own template cache or by a CDN caching the HTML — and the policy has degraded to a static secret that an attacker can read from any cached copy. Fix the caching before continuing. If you are moving from an 'unsafe-inline' policy and cannot stamp nonces onto everything at once, Migrating from unsafe-inline to Hash-Based CSP describes the hash-based intermediate stage.

Step 2 — Bind the Bytes with SRI

Permalink to "Step 2 — Bind the Bytes with SRI"

CSP now controls which tags may create scripts. SRI controls what those scripts contain. The digest must come from the build, not from a developer running a command once and pasting the result, because a hand-maintained digest is a hand-maintained lie the moment the asset changes.

# Emit a manifest of SHA-384 digests for every built asset
find dist/assets -type f \( -name '*.js' -o -name '*.css' \) -print0 \
  | while IFS= read -r -d '' f; do
      digest=$(openssl dgst -sha384 -binary "$f" | openssl base64 -A)
      printf '%s\tsha384-%s\n' "${f#dist/}" "$digest"
    done | tee dist/sri-manifest.tsv

The template then reads that manifest and stamps both attributes onto every tag. Note that the nonce and the digest coexist on the same element — they are not alternatives:

<script type="module"
        src="https://cdn.example.com/assets/app.4f2c1e93.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous"
        nonce="9Yd0Qm2p1s7Kx4Vb8Lz3Ug=="></script>

Every cross-origin tag carrying integrity must also carry crossorigin="anonymous", without exception. The digest is computed over a response body the browser is only allowed to read when CORS permits it; without the attribute the response is opaque, there are no bytes to hash, and the resource is blocked rather than silently accepted. How CORS and crossorigin Affect SRI covers the header combinations that break this.

Scripts your application injects at runtime need the same treatment, set through the IDL properties rather than the markup:

// loader.js — runtime injection that keeps both guarantees intact
import manifest from './sri-manifest.json' with { type: 'json' };

export function loadModule(path) {
  const el = document.createElement('script');
  el.type = 'module';
  el.src = `https://cdn.example.com/assets/${path}`;
  el.integrity = manifest[path];      // fails closed if the entry is missing
  el.crossOrigin = 'anonymous';
  document.head.appendChild(el);
  return new Promise((ok, bad) => {
    el.addEventListener('load', () => ok(el.src));
    el.addEventListener('error', () => bad(new Error(`integrity or network failure: ${el.src}`)));
  });
}

Verification signal. With 'strict-dynamic' active this tag needs no nonce, because trust propagates from the loader that created it. Deliberately corrupt one byte of the target file on a staging CDN and confirm the error handler fires and the console reports a digest mismatch. Deeper patterns for this — including how to fail over when the digest does not match — are in Adding Integrity to Runtime-Injected Scripts.

Step 3 — Close the Sinks with Trusted Types

Permalink to "Step 3 — Close the Sinks with Trusted Types"

The first two layers are now doing their jobs and the page is still one innerHTML assignment away from executing attacker-controlled markup. Trusted Types removes that possibility by type, not by filtering. Register a default policy so that legacy code paths and third-party libraries route through one auditable function, and register named policies for the places you want to be explicit about.

// trusted-types.js — must run before any other script touches a sink
import DOMPurify from 'dompurify';

const ALLOWED_SCRIPT_ORIGINS = new Set([
  self.location.origin,
  'https://cdn.example.com'
]);

if (window.trustedTypes && window.trustedTypes.createPolicy) {
  window.trustedTypes.createPolicy('default', {
    createHTML: (input) => DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: false }),
    createScriptURL: (input) => {
      const url = new URL(input, document.baseURI);
      if (!ALLOWED_SCRIPT_ORIGINS.has(url.origin)) {
        throw new TypeError(`blocked script URL origin: ${url.origin}`);
      }
      return url.href;
    },
    createScript: () => {
      throw new TypeError('string-to-script conversion is not permitted');
    }
  });
}

The default policy is the compatibility hatch: it runs implicitly whenever a string reaches a sink with no explicit policy, which is what lets you enable enforcement without first rewriting every dependency. It is also the piece most likely to be quietly abused, so keep its createScript throwing and review every change to it. Note the trusted-types default app-loader directive from Step 1 — the policy name default must appear there or the call to create it is itself refused. The directive and its 'allow-duplicates' behaviour are covered in Enforcing require-trusted-types-for script, and the sanitizer configuration in Writing a Trusted Types Policy with DOMPurify.

Browser coverage is the one genuinely unsettled part of this stack. Chromium-based browsers have shipped Trusted Types for years; Firefox added support in a 2025 release; WebKit’s implementation has been in progress and you should check current support data rather than assume it before relying on Trusted Types as your only DOM XSS control. Because the API is feature-detected in the snippet above, an engine without support simply runs without the extra guarantee — the page does not break, but it also is not protected, which is exactly why the other two layers stay on.

With all three configured, the value of the arrangement becomes visible when you trace four different attacks through it.

Which layer stops which attack A decision tree branching from a single attack attempt into four techniques: a content delivery network serving altered bytes, an injected script tag pointing at a new host, an inline script written into the HTML output, and an attacker-controlled string assigned to innerHTML. Each branch terminates in the layer that blocks it: Subresource Integrity, Content Security Policy twice, and Trusted Types. Which layer stops which attack Attack attempt CDN serves altered bytes Injected tag from a new host Inline script in HTML output Attacker string into innerHTML SRI digest mismatch CSP no nonce, no load CSP inline refused Trusted Types sink refuses value Remove any one layer and exactly one column reaches execution.

Interaction Rules That Surprise People

Permalink to "Interaction Rules That Surprise People"

The layers are independent by design, but they interact in five ways that regularly cost teams a day of debugging.

'strict-dynamic' disables your host allow-list. This is the rule that generates the most confused bug reports. When 'strict-dynamic' is present in script-src, a conforming browser ignores every host-source and scheme-source expression in that directive, plus 'unsafe-inline'. A team that adds 'strict-dynamic' to a policy that previously listed four partner CDNs will find all four blocked, with a console message that says so explicitly. The fix is not to remove 'strict-dynamic'; it is to load those partners through a trusted loader script so that trust propagates, or to give their tags the nonce.

A nonce does not exempt a tag from its integrity check. The two mechanisms are evaluated at different stages by different algorithms. CSP decides whether the fetch may proceed and the element may execute; SRI decides whether the returned representation matches an approved digest. Both must pass. Teams sometimes drop integrity after adopting nonces on the theory that the nonce is the stronger control — it is not stronger, it is orthogonal, and dropping the digest reopens the CDN substitution path entirely.

Trust propagates, but only to non-parser-inserted scripts. Under 'strict-dynamic', a script created with document.createElement('script') by an already-trusted script inherits trust. A script that arrives through document.write or through markup parsed from a string does not, by design — that is precisely the path an injection would use. Legacy tag managers that use document.write break loudly here, and the correct answer is to update the loader rather than to weaken the policy.

Nonces are not inherited and are deliberately hard to read. A dynamically created element has an empty nonce until you set one, and browsers hide the nonce content attribute from getAttribute and from CSS attribute selectors, exposing it only through the script.nonce IDL property, specifically so that a markup-scraping injection cannot lift it. If you must set a nonce on an injected element, assign el.nonce; copying document.currentScript.getAttribute('nonce') returns an empty string.

require-sri-for is not a control you can currently lean on. The directive exists in specification text and was prototyped behind flags, but it is not enabled by default in current shipping browsers, and prior implementations were withdrawn. Sending it costs nothing and may help later, but the enforceable version of “every script must carry a digest” is a check in your build and a check against the rendered HTML at deploy time. Combining require-sri-for with CSP works through what the directive would and would not buy you.

Set out as a grid, the division of labour is easier to keep straight than any prose summary.

Layer responsibility matrix A three-row comparison matrix. For Subresource Integrity, Content Security Policy and Trusted Types, it lists what each layer binds, what each layer cannot see, and how a failure of that layer surfaces to a developer. What each layer binds, and what it cannot see Layer What it binds What it cannot see How a failure shows SRI integrity attribute bytes to a digest you approved at build time whether that URL should have been loaded at all error event on the element, no execution CSP script-src directive which tags and origins may create a script the content of a script it has already allowed console refusal plus a violation report Trusted Types require-trusted-types-for which strings may reach a DOM sink the network, the bytes, or the serving origin TypeError thrown at the assignment site

Reference Policy for an Application with a Build Step

Permalink to "Reference Policy for an Application with a Build Step"

The following is the complete header set for a typical single-page application served from an origin with a CDN for static assets, one payment iframe provider and one analytics vendor. It is deliberately boring: no 'unsafe-eval', no wildcard origins in the directives that matter, and every relaxation justified in a comment.

# nginx: script policy for an app with content-addressed assets.
# $csp_nonce is set per request by an upstream application server and passed through.
add_header Reporting-Endpoints 'csp-endpoint="https://reports.example.com/csp"' always;
add_header Content-Security-Policy "
  default-src 'self';
  base-uri 'none';
  object-src 'none';
  script-src 'nonce-$csp_nonce' 'strict-dynamic' 'unsafe-inline' https:;
  style-src 'self' 'nonce-$csp_nonce';
  img-src 'self' https: data:;
  connect-src 'self' https://reports.example.com https://api.example.com;
  frame-src https://pay.example-psp.com;
  frame-ancestors 'none';
  form-action 'self';
  require-trusted-types-for 'script';
  trusted-types default app-loader;
  upgrade-insecure-requests;
  report-uri /csp-report;
  report-to csp-endpoint
" always;

The directives that carry the weight of this policy, and the failure mode of getting each one wrong:

Directive or attribute Value used here Why What breaks if it is wrong
script-src 'nonce-…' 'strict-dynamic' 'unsafe-inline' https: Trust flows from the nonce and propagates to loader-created scripts A static nonce or a cached HTML response turns the nonce into a public token
object-src 'none' Plugin content is a legacy script-execution path Flash-era objects and some PDF embeds can execute in old engines
base-uri 'none' Stops an injected <base> retargeting every relative script URL Relative asset URLs can be redirected to an attacker origin
require-trusted-types-for 'script' Turns string-to-code sinks into type errors Without it, DOM XSS remains reachable through first-party code
trusted-types default app-loader Names every policy allowed to exist A policy name absent from this list throws on creation
integrity sha384-… Binds each URL to approved bytes A stale digest blocks a legitimate deploy; a missing one permits substitution
crossorigin anonymous Makes the body readable for hashing Integrity cannot be checked and the resource is blocked
report-to / report-uri both Coverage across engines with different reporting support Violations occur silently in whichever engine you omitted

Choosing SHA-384 over SHA-256 for the digests is a margin decision rather than a correctness one — all three SHA-2 variants named by the specification are acceptable, and SHA-384 gives more headroom at negligible cost.

Rollout Order That Avoids Breakage

Permalink to "Rollout Order That Avoids Breakage"

Enabling all three layers in one deploy is how teams end up rolling all three back. Each layer needs its own report-only period, and the order matters: CSP first because it is the layer that reveals your true script inventory, SRI second because it depends on the build changes CSP forces you to make, Trusted Types last because it produces the largest volume of application-code work.

Script policy rollout timeline A horizontal timeline with five stages: a report-only Content Security Policy with nonces wired in during weeks one and two, Subresource Integrity digests added at the build in weeks two and three, the Content Security Policy switched to enforcing in week four, Trusted Types in report-only mode in weeks five and six, and Trusted Types enforcing in week eight. Rollout order that avoids breakage CSP report-only nonces wired in SRI at build digests in HTML CSP enforcing flip the header Trusted Types report-only Sinks enforcing policy is closed Week 1-2 Week 2-3 Week 4 Week 5-6 Week 8 Each stage stays report-only until its violation stream goes quiet.

During stages one and four, send the report-only header alongside the enforcing one rather than instead of it. Browsers evaluate Content-Security-Policy and Content-Security-Policy-Report-Only independently, so you can keep an already-proven enforcing policy live while trialling a stricter candidate:

curl -sI https://app.example.com/checkout | grep -i '^content-security-policy'
# content-security-policy: base-uri 'none'; object-src 'none'; script-src 'nonce-…' 'strict-dynamic' …
# content-security-policy-report-only: … require-trusted-types-for 'script'; trusted-types default; report-to csp-endpoint

The stage-two work is where most schedules slip, because adding digests exposes every asset whose URL is not content-addressed. An asset served from a mutable URL cannot carry a stable digest, so those assets must move to hashed filenames before SRI can be applied. Budget for that discovery.

Telemetry That Tells You the Policy Is Right

Permalink to "Telemetry That Tells You the Policy Is Right"

An enforcing policy with no reporting is a policy you will weaken the first time someone reports a broken page, because you will have no data to argue otherwise. Configure reporting for all three layers before the first enforcing deploy.

CSP violations arrive at the Reporting-Endpoints target as a JSON array of reports with Content-Type: application/reports+json. A single blocked script produces a body like this:

{
  "age": 12,
  "type": "csp-violation",
  "url": "https://app.example.com/checkout",
  "user_agent": "Mozilla/5.0 …",
  "body": {
    "documentURL": "https://app.example.com/checkout",
    "referrer": "",
    "blockedURL": "https://widgets.example-vendor.com/v3/loader.js",
    "effectiveDirective": "script-src-elem",
    "originalPolicy": "script-src 'nonce-…' 'strict-dynamic' 'unsafe-inline' https:; report-to csp-endpoint",
    "disposition": "enforce",
    "statusCode": 200,
    "sample": ""
  }
}

Trusted Types violations arrive through the same channel with effectiveDirective set to require-trusted-types-for or trusted-types, and — importantly — with a truncated sample of the offending string, which is usually enough to identify the sink. Integrity failures are the odd one out: an SRI mismatch blocks the resource and fires an error event at the element, and you should not assume it will also reach your reporting endpoint, because that behaviour is not consistent across engines. Collect it explicitly:

// telemetry.js — capture what the reporting endpoint may not deliver
window.addEventListener('error', (e) => {
  const el = e.target;
  if (el instanceof HTMLScriptElement || el instanceof HTMLLinkElement) {
    if (el.integrity) {
      navigator.sendBeacon('/telemetry/sri-failure', JSON.stringify({
        url: el.src || el.href,
        integrity: el.integrity,
        page: location.pathname
      }));
    }
  }
}, true); // capture phase: resource errors do not bubble

document.addEventListener('securitypolicyviolation', (e) => {
  navigator.sendBeacon('/telemetry/csp-violation', JSON.stringify({
    directive: e.effectiveDirective,
    blocked: e.blockedURI,
    disposition: e.disposition,
    sample: e.sample,
    page: location.pathname
  }));
});

Four measurements tell you whether the policy is correct rather than merely present. First, the count of distinct blockedURL values from real user traffic: this should trend to zero, and every remaining value should be identifiable as a browser extension or a known injection source rather than a legitimate asset. Second, the ratio of script-src-elem violations to page views, which should be flat — a rising ratio means a vendor changed a loader. Third, the SRI failure rate, which should be zero outside deploy windows; a nonzero steady-state rate almost always means an edge transform is rewriting your assets. Fourth, coverage: the proportion of executing scripts that map to an entry in the build manifest, which you measure by auditing the rendered HTML rather than by reading reports. Alerting thresholds for the second and third of these are covered in Alerting on SRI Failures from CSP Reports, and the endpoint architecture in Security Reporting & Violation Telemetry.

Compliance Mapping

Permalink to "Compliance Mapping"

Auditors do not assess layers; they assess whether a stated requirement has a demonstrable control and evidence. The unified policy maps cleanly onto the frameworks that most frontend teams face, and the mapping is worth writing down before an assessment rather than during one.

Requirement What it asks for Which part of the policy answers it Evidence to keep
PCI DSS v4.0.1 6.4.3 Each script on a payment page is authorised, its integrity is assured, and an inventory with written justification is maintained Nonce issuance and script-src for authorisation; integrity digests for integrity; the build manifest for inventory Signed manifest per release, policy header capture, justification register
PCI DSS v4.0.1 11.6.1 A change- and tamper-detection mechanism alerts on unauthorised modification of HTTP headers and payment page content, evaluated at least weekly Violation telemetry plus an external synthetic check that fetches the page and diffs headers and script tags Weekly synthetic check output, alert history, ticket trail for each alert
SOC 2 CC6.8 Controls to prevent or detect unauthorised or malicious software The enforcing CSP and SRI digests, with the report stream as the detection half Header configuration in version control, sampled violation reports
SOC 2 CC7.2 Monitoring for anomalies indicative of malicious acts Distinct-blocked-URL and SRI-failure-rate metrics with defined thresholds Dashboard screenshots, threshold definitions, incident records
ISO/IEC 27001:2022 A.8.26 Information security requirements are defined for application services The written script policy specification and its directive-by-directive rationale The policy document and its review record
ISO/IEC 27001:2022 A.8.16 Monitoring of systems for anomalous behaviour The reporting endpoint and its alerting rules Retention configuration, alert runbook

The requirement that catches teams unprepared is 11.6.1, because it asks for detection of header modification, not just script modification. A policy header that is silently stripped by a misconfigured edge rule produces no violation reports at all — the page simply becomes unprotected and quiet. The only control that catches this is an external check that fetches the payment page from outside your network and asserts on the headers it receives. If you are already mapping CVE findings to payment-page requirements, Mapping CVEs to PCI DSS 6.4.3 covers the inventory side of the same requirement.

Integration with Adjacent Tooling

Permalink to "Integration with Adjacent Tooling"

The policy is generated, not hand-written, and that generation touches three other systems. The build emits the digest manifest and the list of first-party script URLs; the server or edge renders the nonce and the header from that manifest; the deploy pipeline verifies the rendered HTML against the manifest before traffic is shifted. Where a CDN performs edge transforms — minification, script injection for analytics, HTML rewriting — the transform must run before digests are computed, or every asset it touches will fail its check in production while passing in CI.

For teams whose starting point is a CSP that already exists, Configuring Content Security Policy with SRI covers the header-level details of making the two cooperate, and Deploying Defense-in-Depth Script Controls works through the operational side of running the combined controls across multiple environments.

Troubleshooting

Permalink to "Troubleshooting"

Refused to load the script 'https://widgets.example-vendor.com/v3/loader.js' because it violates the following Content Security Policy directive: "script-src 'nonce-…' 'strict-dynamic' 'unsafe-inline' https:". Note that 'strict-dynamic' is present, so host-based allowlisting is disabled.

The final sentence is the whole diagnosis. 'strict-dynamic' has switched off the https: source expression that used to permit this vendor. Load the vendor through a first-party loader script that already carries the nonce, so trust propagates, or stamp the nonce onto the vendor’s tag if your template renders it. Adding the host back to the directive will not work while 'strict-dynamic' is present.

Failed to find a valid digest in the 'integrity' attribute for resource 'https://cdn.example.com/assets/app.4f2c1e93.js' with computed SHA-384 integrity 'kX9…'. The resource has been blocked.

The bytes the browser received do not match the digest in the tag. The three ordinary causes are a stale manifest from a partial deploy, an edge transform rewriting the file after the digest was computed, and a compression or content-negotiation layer serving a different representation. Compare the computed digest in the message against the one your build produced for that exact file; Debugging SRI Hash Mismatch Errors walks through isolating which of the three it is. Never resolve this by deleting the attribute.

Subresource Integrity: The resource 'https://cdn.example.com/assets/app.js' has an integrity attribute, but the resource requires the request to be CORS enabled to check the integrity, and it is not. The resource has been blocked because the integrity cannot be enforced.

Firefox’s wording for the missing-CORS case. The tag has integrity but no crossorigin="anonymous", or the origin server did not return Access-Control-Allow-Origin. Add the attribute and confirm the CDN sends the header for the asset’s Origin; both halves are required.

This document requires 'TrustedScriptURL' assignment.

Code assigned a plain string to script.src while require-trusted-types-for 'script' was enforcing and no default policy was registered — or the default policy was registered after the offending code ran. Load the Trusted Types bootstrap as the first script in the document, before any framework or vendor bundle, and make sure it is not code-split into a chunk that loads later.

Refused to create a TrustedTypePolicy named 'sanitizer' because it violates the following Content Security Policy directive: "trusted-types default app-loader".

The policy name is not in the trusted-types allow-list. Either add the name to the directive or change the code to use one of the names already listed. This error is common when a dependency creates its own named policy; check the library’s documentation for the name it registers before adding it, and prefer routing the library through your default policy where possible.

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'nonce-…' 'strict-dynamic' 'unsafe-inline' https:". Either the 'unsafe-inline' keyword, a hash ('sha256-…'), or a nonce ('nonce-…') is required to enable inline execution.

An inline script reached the browser without the nonce. The usual causes are an HTML minifier or sanitiser stripping the attribute, a component that renders script tags through a path your nonce injection does not cover, or an inline handler attribute such as onclick, which needs script-src-attr and should be rewritten as an event listener rather than allowed.

Security Boundary Note

Permalink to "Security Boundary Note"

A unified script policy governs which scripts may run, what bytes they consist of, and which strings may become code inside the page. Several categories sit outside that boundary entirely, and stating them plainly is part of the design:

  • A compromised build or template. Every layer here derives its trust from artefacts your pipeline produces. An attacker with commit access or build-server access writes both the script tag and its digest, and issues themselves a nonce. Provenance verification and pipeline hardening address this; script policy cannot.
  • Malicious behaviour in an approved script. A vendor script that passes CSP, matches its digest and never touches a DOM sink can still read forms, set cookies and exfiltrate to an origin your connect-src permits. Isolation — a sandboxed iframe or a separate origin — is the control for that, not integrity checking.
  • Everything outside script-src. Data exfiltration through images, CSS, prefetch hints and DNS is constrained by other directives, and a policy that hardens scripts while leaving connect-src and img-src open has moved the exfiltration path rather than closed it.
  • Engines without Trusted Types. Where Trusted Types is unsupported the DOM XSS class is unmitigated by this stack, which is why sanitising at the sink in application code remains necessary rather than optional.
  • Server-side rendering of attacker data. If untrusted content is interpolated into the HTML template on the server, it arrives as markup rather than as a string reaching a sink, and Trusted Types never sees it. Contextual output encoding on the server is the only control for that path.

Frequently Asked Questions

Permalink to "Frequently Asked Questions"
Does a valid CSP nonce make the integrity attribute redundant?

No. The two checks run independently and answer different questions. The nonce tells the browser that your server authored this particular tag, so the tag is allowed to create a script. The integrity attribute tells the browser which bytes that script must consist of. A nonce on a tag never exempts it from an integrity check, and an integrity match never satisfies a script-src directive that the tag otherwise violates.

Why did adding strict-dynamic stop my CDN allow-list from working?

That is the defined behaviour. When strict-dynamic appears in script-src, browsers that understand it ignore every host-source and scheme-source expression in that directive, along with unsafe-inline. Trust flows only from nonces and hashes, and from scripts created programmatically by already-trusted scripts. Host entries are kept in the header purely as a fallback for older engines that do not implement strict-dynamic.

Can I rely on require-sri-for to make integrity attributes mandatory?

Not in production today. The require-sri-for directive was specified and prototyped, but it is not enabled by default in the current shipping versions of the major browsers, and support has been withdrawn before. Treat it as a defence-in-depth extra if you send it, and enforce the real rule in your build and in a deploy-time check that fails when a script tag reaches the HTML without an integrity attribute.

Do dynamically inserted scripts inherit the page nonce automatically?

No. A script element created with document.createElement has no nonce unless you set one, and the nonce content attribute of the parser-created tag is deliberately hidden from getAttribute to stop it being stolen through markup-scraping attacks. Under strict-dynamic the inserted script does not need a nonce, because trust propagates from the trusted script that created it. Without strict-dynamic, set the IDL property script.nonce.

Which layer actually satisfies PCI DSS v4.0.1 requirement 6.4.3?

None of them alone. Requirement 6.4.3 asks for three things for every script on a payment page: a method that confirms the script is authorised, a method that assures its integrity, and a written inventory with justification. The CSP allow-list and nonce issuance are the authorisation method, the SRI digest is the integrity method, and the build manifest that generates both is the inventory. An assessor will ask to see all three artefacts.

How do I know when the policy is tight enough to stop tuning it?

Watch three signals over a full traffic week. The count of distinct blocked URLs from real users should fall to zero while synthetic injection tests still report. The share of script-src violations attributable to browser extensions and injected content should be stable rather than growing. And every script executing on the page should map to an entry in the build manifest, which an audit script can verify from the rendered HTML.

Permalink to "Related"

Articles in This Topic

Layering CSP Nonces, SRI and Trusted Types
Rolling Out a Script Policy in Report-Only Mode
Auditing a Script Policy for Gaps
Back to Runtime Policy Enforcement & Trusted Types