CSP Nonces Move Script Trust from Hostnames to Response Markup

A script policy based only on hostnames answers a coarse question: which network locations may supply JavaScript? That boundary becomes weak when an allowed origin hosts user-controlled files, JSONP-style endpoints, legacy script resources, or other content that was never intended to receive execution authority.

A nonce-based Content Security Policy changes the unit of trust. Instead of granting execution authority to every script fetched from an approved host, the server places an unpredictable value in the policy and on the specific <script> elements authorized for that response. The browser checks that relationship before executing those elements.

This is not a general input-sanitization mechanism. It is an execution boundary enforced by the browser, and its strength depends on where the nonce is generated, where it is copied, and which scripts are allowed to propagate trust.

The nonce binds policy to selected elements

A response can carry a policy such as:

Content-Security-Policy: script-src 'nonce-r4Nd0mV4lu3'; object-src 'none'; base-uri 'none'

The document can then mark an intended script:

<script nonce="r4Nd0mV4lu3" src="/assets/app.js"></script>

The nonce value is not a shared application secret. Its useful property is freshness and unpredictability for the response in which it appears. A new response needs a new nonce. Reusing a stable value turns an authorization token intended for one document instance into a value that can be copied across responses.

The server must also avoid attaching the nonce mechanically to every script-shaped fragment in generated markup. If attacker-controlled HTML reaches a template stage that automatically decorates every <script> element with the current nonce, injected script can receive the same authority as application script. The policy then faithfully enforces a bad server-side trust decision.

The relevant boundary is therefore not merely “nonce present.” It is “nonce copied only onto script elements selected by trusted rendering logic.”

Host allowlists and nonce policies authorize different things

A host allowlist such as:

Content-Security-Policy: script-src 'self' https://static.example.net

authorizes script retrieval according to source location. Any resource that satisfies the source expression can potentially enter the script execution path, subject to the rest of the browser’s checks.

A nonce policy authorizes elements rather than an entire host namespace. An external script can carry a nonce just as an inline script can. This distinction matters on origins that expose more content than the security policy actually intends to execute.

The shift also changes maintenance. Moving a trusted script between hosts does not inherently require widening a source allowlist if the element itself remains nonce-authorized and the deployed policy does not add separate restrictions that prevent the fetch. Conversely, adding a new script to a page requires the rendering path to mark that element deliberately.

Nonce policies do not make script provenance irrelevant. The authorized script still has the privileges of script executing in the document. If that script is compromised at its source, built from compromised dependencies, or contains a dangerous data-to-code path, a valid nonce does not repair it. CSP controls browser execution eligibility; it does not attest to software integrity.

strict-dynamic extends trust through script creation

Modern applications often load script dependencies at runtime. Without an additional rule, a nonce-authorized bootstrap script can still encounter CSP blocks when it creates further script elements whose URLs are not otherwise allowed.

The 'strict-dynamic' source expression changes that behavior. In supporting user agents, trust granted through a valid nonce or hash can propagate to scripts loaded by a trusted root script through non-parser-inserted script elements. A policy can therefore look like:

Content-Security-Policy: script-src 'nonce-r4Nd0mV4lu3' 'strict-dynamic'; object-src 'none'; base-uri 'none'

This is a substantial trust expansion. The bootstrap script is no longer authorized only to execute its own code; its script-loading decisions can also authorize descendants. Code that turns attacker-controlled strings into script URLs becomes especially significant under this model.

'strict-dynamic' also changes the treatment of source expressions in supporting CSP implementations. Host sources, scheme sources, 'self', and 'unsafe-inline' in the same script-src directive do not act as additional script allowlists once 'strict-dynamic' is active with a valid nonce or hash. Compatibility policies may still include older expressions for user agents implementing earlier CSP levels, so effective behavior can differ by CSP support level.

The deployment question is therefore architectural: which root scripts deserve delegation authority, and can their dynamic loading paths be influenced by untrusted data?

Nonce generation belongs at the response boundary

A nonce has to be unpredictable enough that an attacker who can inject markup cannot guess a value accepted by the browser. It also has to be unique per response.

That requirement places nonce generation close to response construction. A reverse proxy, application server, edge renderer, or another trusted response component can generate the value, but the same value must reach both the CSP header and the authorized markup without becoming a reusable global constant.

Caching complicates this arrangement. A full-page cache that stores HTML containing a nonce together with a matching CSP header can replay the same nonce to many clients. A cache that varies the header but not the HTML can instead create mismatched values and break intended scripts. Systems that cache rendered HTML need a design that preserves per-response freshness, such as late substitution at a trusted layer, or a different CSP strategy such as hashes for genuinely static script markup.

The exact cache architecture is deployment-specific. The invariant is simpler: the browser must receive one internally consistent response, and a nonce from that response must not become a predictable authorization value for later responses.

A nonce can leak without becoming a standalone credential

The nonce appears in the document and is available to browser processing. Treating it like a password leads to the wrong threat model. CSP nonce protections are designed around preventing an injected markup fragment from gaining execution authority merely by existing in the page.

The critical failure is not that the value can ever be observed inside the document environment. The critical failure is giving attacker-controlled markup a path to obtain or inherit a valid nonce on an executable element, or allowing already trusted script to transform attacker-controlled data into executable script.

This distinction keeps the security review focused on rendering and execution flows rather than on trying to hide a value that necessarily participates in document markup.

CSP reports show enforcement events, not exploitability

A deployment can first observe policy effects with Content-Security-Policy-Report-Only. This mode records violations without enforcing the policy, which is useful for finding script paths that a proposed policy would block.

Violation reports are telemetry. A blocked script does not prove an exploit attempt, and the absence of reports does not prove that all injection paths are safe. Browser coverage, reporting configuration, extensions, network failures, sampling, and application traffic all affect what reaches a collector.

Enforced CSP and report-only CSP also serve different purposes. Report-only mode can expose compatibility problems before enforcement, but it provides no execution barrier. Moving to enforcement is the point at which the browser begins rejecting script execution that violates the active policy.

The strongest boundary is narrow execution authority

Nonce-based CSP is most valuable when it reduces the set of markup that can acquire script execution authority. That requires more than adding a random string to a header. Nonce generation must be per response, trusted rendering must control nonce placement, and delegated loading through 'strict-dynamic' must be treated as authority rather than convenience.

The result is a narrower trust model than a broad hostname allowlist. Network location can still matter for transport and software supply, but the browser’s execution decision is tied to specific response markup. That separation is useful precisely because an origin boundary and a script-authority boundary are not always the same thing.