CSP Strict-Dynamic Shifts Script Trust From Hosts to Execution Lineage
A page can carry a restrictive script host list and still face a difficult deployment choice when its trusted bootstrap code loads dependencies at runtime. Adding every current script host to script-src keeps policy tied to network locations that can change. Adding 'strict-dynamic' takes a different route: in supporting browsers, trust attached to a nonce-bearing or hash-authorized root script can propagate to scripts that root code inserts dynamically.
That change is deeper than a shorter allowlist. It moves the main authorization question from the origin serving a script to the execution lineage that caused the script to load. The resulting policy can resist classes of markup injection that exploit broadly trusted hosts, but it also makes trusted loader behavior a critical security boundary.
A nonce authorizes an element, not a host
A nonce source in script-src lets a document mark selected script elements with a response-specific value. The browser compares the element’s nonce with the nonce source in the policy. A matching value can authorize that script even when its network location is not otherwise present in a host source list.
The value has to be unpredictable and fresh for the response. Reusing a stable nonce turns a per-response authorization token into a value that can potentially be copied into injected markup. The server also has to place the nonce only on script elements it intends to authorize. A template transformation that blindly adds the current nonce to attacker-controlled script markup defeats the distinction the mechanism is meant to create.
A hash source provides a related trust anchor without requiring a fresh response value. It authorizes script content matching the declared digest under the applicable CSP rules. That fits static script material more naturally, while nonce policies fit pages whose server-side rendering can stamp selected elements per response.
Neither mechanism asserts that a host is generally safe. The authorization is attached to a particular root script through a nonce or content digest.
Strict-dynamic propagates that root trust
With 'strict-dynamic' in script-src, a script accepted through a valid nonce or hash can create script elements whose loads inherit trust. The propagation can continue through further dynamically loaded descendants.
This behavior supports loader architectures in which a small trusted bootstrap selects bundles, chunks, or dependencies at runtime. A host-centric policy would need source expressions covering the locations of those resources. A strict-dynamic policy can instead authorize the bootstrap and rely on its script-loading decisions.
The propagation is not a general exemption for every script that appears after the root executes. The distinction between parser-inserted script elements and script elements created through trusted script execution matters. A bare script tag injected into document markup does not gain trust merely because an authorized bootstrap exists elsewhere on the page.
In CSP implementations that apply strict-dynamic semantics, host sources, scheme sources, 'self', and 'unsafe-inline' in that script source list no longer provide the effective script authorization path. This property is also used for compatibility policies: older CSP implementations can fall back to source expressions they support, while newer implementations apply the nonce or hash plus strict-dynamic model.
The loader becomes a policy decision point
Trust propagation changes the security review target. If a nonce-authorized bootstrap creates a script element from a fixed internal mapping, its descendants remain constrained by that mapping even though CSP itself is not enumerating their hosts. If the same bootstrap accepts an attacker-influenced URL and assigns it directly to a new script element, strict-dynamic can authorize the resulting load because the browser sees it as a descendant of trusted script execution.
The browser is enforcing provenance at the script-loading boundary, not proving that the loader’s input is safe.
Consider a bootstrap that receives a module identifier from application state. A design that maps a closed identifier set to fixed URLs keeps the decision inside trusted code. A design that treats an arbitrary string as a script URL delegates code selection to whatever can influence that string. CSP cannot recover the lost constraint after trusted code has chosen to create the script element.
This is the central trade-off. Strict-dynamic reduces dependence on a sprawling network allowlist, but it raises the security significance of every trusted script path that can construct or append executable script elements.
Host allowlists and execution lineage fail differently
A host allowlist answers whether a script URL matches an approved source expression. Its security depends partly on the content served from those approved locations. A broad content host, user-controlled path, JSONP-style endpoint, or other script-capable surface can make a nominally trusted host a larger execution boundary than intended.
Nonce or hash roots narrow the initial authorization to selected script elements. Strict-dynamic then permits descendants based on trusted creation rather than their host identity. This can avoid granting blanket script privilege to every executable resource on a domain.
The replacement boundary has its own failure mode. A trusted loader with a script-injection gadget can become a route around the location restrictions that strict-dynamic intentionally stops using. The dangerous capability is no longer merely “content can be served from an approved host”; it is “trusted code can be induced to create a script from attacker-selected input.”
These are materially different review problems. Host policy calls for scrutiny of source breadth and content control. Execution-lineage policy calls for scrutiny of loader APIs, URL construction, data flow into script creation, and code paths that turn strings into executable resources.
Nonce secrecy is not the full model
Treating the nonce as a conventional long-lived secret can obscure its actual role. The important property is that injected markup cannot predict the value for the response and attach it to a new script element before policy enforcement.
Browsers also limit direct exposure of nonce values through ordinary attribute access patterns, but application design should not rely on hiding a nonce from script that is already authorized to execute. Code running with page privileges can often perform actions that are already security-sensitive. CSP is primarily a mitigation boundary against unauthorized script execution, not a sandbox between mutually hostile scripts that have all been admitted by the same page.
This also means nonce generation and HTML caching have to agree. A response-specific nonce embedded in both the CSP header and document markup cannot be independently cached or substituted without preserving that pairing. A cache that serves HTML carrying one nonce with a policy carrying another causes legitimate scripts to fail. A cache that intentionally reuses a complete nonce-bearing response extends the lifetime of that response’s nonce beyond a fresh-generation model.
Operational correctness therefore includes the response assembly path, not only the policy string.
Script execution sinks remain relevant
A strict CSP does not make trusted JavaScript safe from its own dangerous data flows. If authorized code passes attacker-controlled strings into an execution sink that CSP permits, the page can still lose its intended boundary.
For example, allowing 'unsafe-eval' re-enables string-to-code mechanisms governed by that source expression. Strict-dynamic does not neutralize that choice. Similarly, a trusted script that converts untrusted data into a script URL and inserts a new element can propagate its trust to that load.
The policy is strongest when the application minimizes both the number of trust roots and the capabilities those roots expose to attacker-influenced data. This is an implementation property rather than a header-only property.
DOM injection controls can complement CSP. Trusted Types, where supported and correctly deployed, targets selected DOM injection sinks by requiring values created through approved policies. It addresses a different enforcement point and does not replace nonce generation or strict-dynamic script authorization. Combining controls can narrow separate paths, but their guarantees remain conditional on the exact directives, browser support, and application code.
Reporting exposes violations, not proof of safety
CSP reporting can surface blocked resource loads and other policy violations. Report-only deployment can also reveal compatibility problems before an enforcing policy is activated. These signals are valuable during migration because a strict-dynamic transition can expose hidden dependencies on parser-inserted scripts, inline handlers, or source-list assumptions.
A quiet report stream is not evidence that trusted loaders are free of unsafe data flow. If trusted code successfully creates a descendant script under strict-dynamic, that action is consistent with the policy and need not appear as a violation. Security review still has to inspect the code that holds propagated script-loading authority.
Reporting also depends on delivery, endpoint availability, browser behavior, sampling decisions outside CSP itself, and the application’s ability to process reports safely. It is telemetry around enforcement, not a substitute for enforcement or code review.
Compatibility policy can carry two trust models
A policy may include a nonce, 'strict-dynamic', and fallback source expressions for older CSP implementations. That pattern deliberately produces different effective behavior across browser generations.
A modern implementation that supports strict-dynamic can ignore host and scheme sources in the relevant script source list and rely on nonce or hash roots plus propagated trust. An older implementation that lacks strict-dynamic may instead apply the source expressions it recognizes.
This compatibility technique has a security consequence: the fallback is part of the deployed security posture for clients that use it. A permissive fallback does not weaken strict-dynamic processing in a supporting browser, but it can define a broader boundary for older clients. Support requirements therefore affect the minimum policy behavior the service actually delivers across its client population.
The same distinction matters during debugging. A script that loads in one browser because of a fallback host source may be blocked in a browser applying strict-dynamic because it lacks an authorized lineage. Apparent inconsistency can be the expected result of two CSP generations interpreting the same source list under different rules.
Execution lineage is powerful only when its roots are narrow
Strict-dynamic turns script loading into a delegated trust system. The page grants initial authority through nonce or hash sources, and authorized scripts can extend that authority to dynamically inserted descendants. That architecture can remove brittle host enumeration and reduce exposure created by broad script source lists.
Delegation also concentrates risk. A root script is not merely another allowed resource; it can become an issuer of script-loading authority. Its URL construction, dependency resolution, DOM insertion logic, and handling of attacker-influenced values all sit on the enforcement boundary.
The security gain comes from making initial trust precise and keeping delegated decisions constrained. When trusted code treats arbitrary input as executable location data, strict-dynamic faithfully propagates trust through a path the application itself made unsafe. When roots are few and loaders keep code selection inside controlled mappings, the policy expresses a much narrower script boundary than a domain-wide allowlist can provide.