CSP Nonces Bind Script Execution to Server-Selected Markup

A Content Security Policy (CSP) can restrict which scripts a browser executes. Host-based source lists are one way to express that restriction, but a permitted host is a coarse trust boundary: any script resource matching the allowed source can satisfy that part of the policy.

A nonce-based policy changes the unit of authorization. The server generates an unpredictable nonce for a response, places the nonce in the script-src policy, and attaches the same value only to script elements selected for execution. The browser compares those values when applying CSP.

The useful property is response-scoped authorization. A nonce is not a permanent credential for a script URL or origin, and it should not become one through reuse.

The response carries both policy and authorization marker

A response can send a policy such as:

Content-Security-Policy: script-src 'nonce-r4Nd0mValue'

The corresponding markup carries the same nonce on an intended script element:

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

For nonce-source matching, the browser checks the element’s nonce against the nonce source in the active policy. A matching nonce can authorize that script element even when its URL is not represented by a host source expression.

This moves a significant decision to response generation. The application is no longer saying that every script from a broad location is acceptable. It marks particular script elements in the generated document as trusted for that response.

A nonce must be fresh and unpredictable

The security value of a nonce depends on an attacker being unable to predict a valid value for the response they are attacking. Reusing a fixed nonce across responses turns a per-response authorization marker into a reusable value.

A suitable generation pattern is conceptually simple:

request
   |
   v
cryptographically strong random nonce
   |
   +----> CSP response header
   |
   +----> intended <script> elements

The nonce does not need to be secret after the response reaches the browser. It appears in the document and policy. Its protection comes from unpredictability before the server emits the response and from avoiding reuse across independently generated responses.

Caching therefore needs deliberate handling. If HTML containing a nonce is cached together with its CSP header, replaying that cached response also replays the nonce. Whether that is acceptable depends on the response architecture and threat model; a design that relies on fresh response-scoped nonces must keep cache behavior consistent with that requirement.

A nonce does not sanitize attacker-controlled markup

CSP is a browser enforcement layer, not an HTML sanitizer. An application still has to encode or sanitize untrusted data for the context where it is inserted.

The dangerous boundary appears when attacker-controlled data can acquire a valid nonce. For example, a template must not copy a nonce into arbitrary markup merely because a fragment contains a script element. The server must control which elements receive the authorization marker.

trusted template path ----> script + valid nonce ----> eligible

untrusted input ----------> injected script --------> no nonce

If an injection flaw lets an attacker place script content inside an already trusted nonced script block, the nonce cannot distinguish trusted bytes from attacker-controlled bytes inside that block. The application still needs safe data handling at the injection boundary.

Hash sources fit static script content

CSP also supports hash sources. Instead of assigning a fresh marker to an element, the policy can contain a cryptographic hash of permitted inline script content. The browser hashes the relevant script content and compares the result with the policy.

That model is attractive when inline script bytes are stable. Any content change requires a corresponding policy hash change. Nonces fit server-rendered documents where response generation can attach a fresh value to selected script elements.

Neither mechanism turns arbitrary dynamic code into safe code. Both mechanisms identify script content or elements that the policy is prepared to authorize.

strict-dynamic changes trust propagation for scripts

CSP Level 3 defines the 'strict-dynamic' source expression for script policies. In supporting user agents, it changes how script trust can propagate from a script authorized by a nonce or hash to scripts that trusted code loads through non-parser-inserted script elements.

A policy can take this form:

Content-Security-Policy: script-src 'nonce-r4Nd0mValue' 'strict-dynamic'

This is materially different from adding more hostnames to an allowlist. The initial trust anchor is the nonce- or hash-authorized script. Script loading initiated by that trusted script can then participate in the strict-dynamic behavior defined by CSP.

That propagation is powerful and should remain intentional. If a trusted bootstrap script accepts attacker-controlled URLs and creates script elements from them, CSP cannot repair the bootstrap script’s own unsafe trust decision. The application has delegated script-loading authority to code that must handle its inputs accordingly.

Source lists and nonces express different boundaries

Consider a host allowlist:

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

The browser can accept script requests matching that source expression. The policy boundary is the allowed source.

Now compare a nonce policy:

Content-Security-Policy: script-src 'nonce-r4Nd0mValue'

The authorization is attached to selected script elements in the response. This can avoid treating an entire permitted host as equivalent to a specific script chosen by the document generator.

The distinction matters when an allowed host serves many resources, accepts user-controlled content, redirects requests, or changes independently from the application. A narrower authorization primitive reduces the amount of script namespace represented directly in the CSP source list, but it does not remove the need to assess the behavior of scripts that are actually trusted.

Report-Only can expose policy breakage before enforcement

Deploying a stricter script policy can break pages that still depend on inline handlers, dynamically generated script elements, or third-party loaders that do not fit the intended trust model. CSP provides a report-only delivery mode so a policy can be evaluated without enforcing its restrictions.

Content-Security-Policy-Report-Only: script-src 'nonce-r4Nd0mValue'

Report-only telemetry is useful for locating policy violations, but receiving no reports is not proof that every execution path is covered. Reporting can be affected by client behavior, delivery configuration, sampling choices, and paths that were not exercised during observation.

Enforcement should therefore follow application-specific testing rather than treating telemetry as a formal completeness check.

CSP remains one layer in XSS defense

A strong nonce-based CSP can make many script-injection paths harder to turn into script execution because injected elements lack the server-selected authorization marker. That is a meaningful containment property, but it is not a substitute for fixing injection flaws.

Output encoding, contextual sanitization, safe DOM APIs, dependency control, and careful template boundaries still matter. CSP adds a browser-enforced restriction around script execution when other controls fail or when application complexity leaves an unexpected injection path.

The clean design boundary is narrow: generate a fresh unpredictable nonce for the response, expose it only through the policy and script elements the server intends to authorize, keep untrusted markup away from that authorization path, and treat trusted scripts as code with real script-loading authority. That keeps CSP focused on execution control rather than asking it to compensate for unsafe data flow elsewhere in the application.

References