What a CSP nonce authorises
The server puts a value in the script-src directive and the same value in the nonce attribute of the intended script elements. The browser can then authorise those elements. This works for inline scripts and external script elements. For style elements, there is an equivalent path through the style directive; a script nonce does not automatically permit styles. MDN: nonces in CSP
A nonce sanitises neither a script's content nor the data it later uses. Mark only elements that you intend to execute. A subsequent pass that adds a nonce to every script it finds could also authorise injected code.
Here, nonce does not mean a user login or permission check. It is an authorisation within CSP's rules. If the matching nonce is absent, this particular authorisation path fails. Other effective sources, hashes or dynamic loading paths may still permit the script; additional enforced policies may block it.
The specification requires a new value each time a policy is transmitted. It recommends at least 128 bits before encoding and a cryptographically secure random number generator. The same value can mark several authorised elements in one response. Retrieving a single header proves neither unpredictability nor absence of reuse nor correct assignment in the HTML. W3C: nonce requirements
Limits of the evidence: CSP can make script injection harder. A nonce or hash alone, however, establishes neither a complete script inventory nor ongoing integrity or tampering checks, nor NIS2, PCI DSS or data protection compliance. For relevant payment scripts, PCI SSC describes additional requirements in the context of PCI DSS 4.0.1. PCI SSC: payment page security
Connect generation, header and HTML
1. Generate a suitable random value
Use a cryptographically secure random number generator. Math.random() is not a suitable cryptographic source. Each of the following independent snippets generates 16 random bytes and encodes them as Base64. The two Node.js variants and the Python snippet were executed in the original local check; the PHP snippet was checked against the vendor documentation, not in a PHP runtime.
Node.js, CommonJS:
const crypto = require('node:crypto');
const nonce = crypto.randomBytes(16).toString('base64');Web Crypto in Node.js, CommonJS:
const { webcrypto } = require('node:crypto');
const array = new Uint8Array(16);
webcrypto.getRandomValues(array);
const nonce = Buffer.from(array).toString('base64');The second variant demonstrates the Web Crypto interface in an explicitly named server environment. A comparable API also exists in browsers, but a value generated there after delivery does not retrospectively set the CSP response header already received. Generation must be connected to your actual HTML and header output. Node.js: randomBytes and Web Crypto
PHP:
<?php
$nonce = base64_encode(random_bytes(16));PHP: random_bytes supplies cryptographically suitable random bytes. If generation fails, a fixed fallback token must not silently take their place.
Python:
import secrets
import base64
nonce = base64.b64encode(secrets.token_bytes(16)).decode('ascii')Python: secrets is intended for security-sensitive random values. The explicit byte count avoids depending on a function default.
16 bytes correspond to 128 bits. In padded Base64, they produce 24 characters including the final ==. The character count of an arbitrary token does not prove entropy, however; a fixed 24-character value remains fixed.
2. Put the value in the CSP header
In the directive, nonce- precedes the value, and the entire source expression is enclosed in single quotes. The HTML attribute contains only the value. The following fixed value is solely a synthetic placeholder, not a generated measurement result or production token. It must be replaced during actual delivery.
Content-Security-Policy: default-src 'self'; script-src 'nonce-N4M0bqjA3jEwA8aSPT9V1w==' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'The policy illustrates how the sources interact. It is not a universal configuration: 'unsafe-inline' deliberately permits inline styles here; object-src 'none' blocks the corresponding embeds, and base-uri 'self' restricts the base URL. Whether these values suit the application remains open. The header value occupies one line.
3. Mark only intended elements
The next elements belong to the model just shown. This assumes a supporting browser, enforcement, no differing script-src-elem rule and no additional blocking policy. Under these conditions, the matching nonce authorises the first two elements; the third inline script without a nonce would be blocked. This explains a rule, not a browser result.
<script nonce="N4M0bqjA3jEwA8aSPT9V1w==">
console.log('Authorised example');
</script>
<script nonce="N4M0bqjA3jEwA8aSPT9V1w==" src="/main.js"></script>
<script>console.log('Example without a nonce');</script>The header and authorised elements of the same response must use the same value. Updating only the header or only the HTML is not enough. Also check additional policies: a valid nonce in one policy does not override a restriction in another.
Schematic explanation · not a measurement
The header and HTML must match
- Generate a fresh value for this document response
Use a cryptographically suitable random source; at least 128 bits before encoding are recommended.
VALUEis only a label here. - Put the same value in both places
Response header
script-src 'nonce-VALUE'In the policy, the
nonce-prefix comes before the value.Selected HTML elements
nonce="VALUE"The attribute contains only the value. Several intended scripts in the same response can share it.
- Deliver the header and HTML together
Do not reuse a completed nonce across multiple document responses. The application and cache must handle both sides of the pair together.
- The browser compares the values
A match satisfies this nonce authorisation path. Without a matching value it fails; other permissions and additional policies affect the final execution decision.
Mark only intended elements. A subsequent pass over every script could also authorise injected code. A matching nonce does not check script content.
What strict-dynamic changes
'strict-dynamic' permits scripts not inserted by the HTML parser in the relevant script context. A script authorised by a nonce or hash can thus load further scripts, for example with document.createElement('script'). Insertion through document.write() is not automatically covered. W3C: strict-dynamic
Under the model policy above, strict-dynamic would permit the following loading path. This code was not executed; the file on cdn.example.com is an example destination.
<script nonce="N4M0bqjA3jEwA8aSPT9V1w==">
const script = document.createElement('script');
script.src = 'https://cdn.example.com/library.js';
document.head.appendChild(script);
</script>In supporting browsers, host and scheme sources, 'self' and 'unsafe-inline' are ignored for this script evaluation. Nonces and hashes remain relevant. Without strict-dynamic, the subsequently loaded element needs another suitable authorisation path; permission for a host is one possibility, not the only one.
The common term “chain of trust” describes delegated execution permissions here. It does not establish that the loaded content is trustworthy. If an attacker can control the URL used by the loader, this path can be misused. Use strict-dynamic only with controlled loading paths and an actual need. The CSP3 draft expressly advises avoiding the mechanism where it is not needed. W3C: strict CSP
Compatibility values are additional permissions
The following historical compatibility pattern has different effects depending on the supported CSP level. NONCE_VALUE is a placeholder:
script-src 'nonce-NONCE_VALUE' 'strict-dynamic' 'unsafe-inline' https:On narrow screens, scroll the table sideways.
| Supported model | Effective script permission in the example |
|---|---|
| CSP1 | 'unsafe-inline' and https:; broad permission for inline and HTTPS scripts. |
| CSP2 | The nonce and https:; 'unsafe-inline' is ignored because of the nonce expression, and strict-dynamic is unsupported. |
| CSP3 with strict-dynamic support | Nonce and dynamic loading path; the listed inline, host and scheme fallbacks are ignored. |
This explains the fallback effect, rather than recommending “maximum security” in every browser. Decide which older permissions are acceptable based on your target browsers. MDN: script-src and strict-dynamic
Avoid generation and caching mistakes
On narrow screens, scroll the table sideways.
| Topic | What to check | Problematic counterexample |
|---|---|---|
| Random source | A cryptographically suitable API, such as crypto.randomBytes(16). | Math.random().toString(36) as a security token. |
| Length | Recommendation: at least 128 bits before encoding. | Short or fixed values; length alone treated as proof of entropy. |
| Reuse | A new value each time a policy is transmitted; authorised elements of that response may share the value. | A constant value across different responses. |
| Match | Match the attribute value exactly to the value after nonce-. | Additional whitespace, as in nonce=" NONCE_VALUE ". |
| Caching | Handle the header and authorised HTML elements together for delivery. | Continually reuse a nonce from a full-page cache, or refresh only one side of the pair. |
A CDN cache containing finished nonce-bearing HTML does not meet the freshness requirement on its own. A suitable delivery step would have to regenerate the policy and only the authorised elements consistently. Indiscriminately marking every script afterwards is unsuitable. Hashes may suit static content better. MDN: nonces and hashes
Cache-Control: no-store can be part of a suitable strategy for dynamically generated HTML. It does not generate a nonce, automatically remove previously stored responses or prove correct CDN or application configuration. Check the caches actually involved and the consistency of header and HTML; a directive alone does not replace this comparison. RFC 9111: no-store
British Airways 2018: a nonce is not a content check
In its decision of 16 October 2020, the UK's data protection authority, the ICO, describes how an attacker modified an existing JavaScript file on the British Airways website after gaining network access. The change was used to extract card data. The fine imposed was £20 million. ICO: British Airways Penalty Notice, paragraphs 1.2 and 1.7
This does not establish that a nonce would have prevented that particular attack. If an authorised external script element points to a modified file, its nonce does not check the file's bytes. Other mechanisms, such as Subresource Integrity, can address file integrity; their effect also depends on the particular integration. This historical account is not an executed CSP counter-test.
Compare unsafe-inline and nonces
Without a nonce or hash effective in the same inline context, 'unsafe-inline' permits arbitrary inline scripts. Supporting browsers ignore it in a corresponding nonce or hash source list. Consider script-src-elem and script-src-attr separately: an explicit attribute rule can continue to allow event handlers. A nonce on a script element does not authorise arbitrary neighbouring onclick handlers. MDN: script-src
On narrow screens, scroll the table sideways.
| Model | Expected effect on an additional inline script without a nonce |
|---|---|
script-src 'self' 'unsafe-inline' | This script rule would permit it. |
script-src 'nonce-NONCE_VALUE' 'strict-dynamic' | This script rule would block the script placed directly in the HTML without a nonce. |
This assumes enforcement, browser support and no other effective script-element rule. NONCE_VALUE represents a correctly generated value. The table is not based on a measurement; it promises neither successful data theft in the first case nor an entirely secure page in the second.
Schematic explanation · not a measurement
The same inline insertion under different rules
Shared case: an additional inline script placed directly in the HTML without a nonce. Prerequisites: enforcement, browser support and no other effective script-src-elem rule.
Rule with broad inline permission
script-src 'self' 'unsafe-inline'
This rule permits execution. Another enforced policy may impose further restrictions.
Rule with targeted authorisation
script-src 'nonce-NONCE_VALUE' 'strict-dynamic'
This rule blocks this direct inline insertion. NONCE_VALUE represents the freshly generated value; the additional script does not have it.
A conditional comparison of rules, not an overall security assessment. Permitted code, dynamic loading paths and other functionality still need separate checks. The figure claims neither data theft nor a completely secure application.
Nonce or hash: decide according to delivery
On narrow screens, scroll the table sideways.
| Scenario | Useful starting point |
|---|---|
| Server-generated HTML, for example with Node.js, PHP or Python | Nonces, if the header and authorised elements can be generated together for each response. |
| Static HTML pages without that generation step | Hashes for fixed script content. |
| Single-page application, for example with React, Vue or Angular | Decide according to actual HTML delivery: a hash may suit static delivery, a nonce dynamic delivery. The framework name does not decide this. |
| Inline scripts known at build time | Hashes; update the values and policy together when content changes. |
| Dynamically generated script content | Nonces may be suitable, provided the authorised content is generated safely. |
Nonces and hashes can be combined. That does not make a reused nonce in a fully cached response correct. For a particular integration, examine the generation and caching paths and the scripts actually emitted.
Complete response pair and our browser check
The following CommonJS function was used in the original local Node.js 24.3.0 lab; the example document's language and title are translated here. It generates a new document response on every call. Two intended inline scripts and an external script served by the application share that response's nonce. Your application must serve the fixed example file /main.js itself; in the check, it merely set an observable test variable.
const { randomBytes } = require('node:crypto');
function createDocument(label) {
const nonce = randomBytes(16).toString('base64');
const data = JSON.stringify({ label: String(label) })
.replace(/</g, '\\u003c')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
const headers = {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store',
'Content-Security-Policy': `default-src 'self'; script-src 'nonce-${nonce}' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'none'`,
};
const html = `<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Local nonce example</title>
<h1 id="output"></h1>
<script nonce="${nonce}">
window.exampleData = ${data};
document.getElementById('output').textContent = window.exampleData.label;
</script>
<script nonce="${nonce}">
document.body.dataset.secondScript = 'executed';
</script>
<script nonce="${nonce}" src="/main.js"></script>
</html>`;
return { headers, html };
}
module.exports = { createDocument };
Call createDocument(label) within the actual document request. Pass the returned headers to the HTTP response, followed by exactly the returned HTML as its body. Do not generate a second nonce for this or store the finished pair for unchanged reuse in later document responses. In an existing application, this step must fit its response API and caching path.
Safe embedding is an additional task: Here, JSON.stringify first produces a JavaScript data value. Replacing < prevents a string such as </script> from closing the HTML script block prematurely; the two Unicode line separators are also escaped. Output uses textContent. The nonce itself comes exclusively from the generator and contains no user input. This handling is intended for this context, not as a universal encoder for HTML, URLs or other templates.
On narrow screens, scroll the table sideways.
| Controlled case | Observation in all three engines | Meaning |
|---|---|---|
| Two fresh document responses | Different values; within each response, the header and all three attributes matched, each decoding to 16 bytes. | This sample demonstrates freshness and assignment. Cryptographic suitability comes from the documented API, not from two different values. |
| Matching response pair | Both inline scripts and the marked external script executed. | Several intended elements in the same response can share the value. |
| Nonce absent or incorrect | All three scripts blocked. | The matching authorisation path is absent under this enforced policy. |
| New header with HTML from a different response | Scripts blocked. | Separate generation or incorrectly assembled cached responses break the match. |
| Finished pair deliberately reused twice | Scripts ran both times with the same nonce. | Working functionality alone does not prove correct freshness. no-store does not repair deliberate reuse by the application. |
| Label containing a closing script sequence | Visible as text; no additional script executed. | The illustrated data embedding withstood this particular counterexample. |
strict-dynamic and a controlled local loader | A script loaded through createElement executed; an additional script placed directly in the HTML without a nonce was blocked. | Dynamic loading and parser insertion are different cases. |
Additional enforced script-src 'none' policy | No script execution despite matching nonces. | A matching permission does not override a second restriction. |
Broad inline permission with 'unsafe-inline' | The now unmarked inline scripts executed. | The figure's comparison applies under its stated conditions. |
The shared CSP check covered 13 cases per engine, including the Report-Only/enforcement comparison. The three generator snippets ran separately with Node.js 24.3.0 and Python 3.9.6. PHP, a real CDN cache, third-party script services, Safari, iPhone and production applications were not tested. The deliberate caching counterexample was reuse by our own test server.
Related guides
The Web Security Check can provide an initial finding about the CSP headers of the retrieved response. Such a request does not establish secure random values, absence of reuse, matching HTML attributes or working scripts.
Frequently asked questions
What is a CSP nonce?
An unpredictable value with which a policy authorises selected script or style elements. For scripts, the same value appears in the appropriate CSP directive and the nonce attribute. Other sources and policies can affect the final decision.
How long should a CSP nonce be?
At least 128 bits before encoding are recommended, for example 16 cryptographically generated random bytes. Padded Base64 represents these as 24 characters. A sufficiently long but fixed or predictable value is not enough.
Do nonces work with CDN caching?
Not by reusing complete nonce-bearing responses unchanged. A suitable delivery step must handle the header and authorised HTML elements consistently for each response. Hashes may suit static content better. A no-store header alone does not prove that this coordination works.
Can I use nonces and hashes together?
Yes: for example, nonces for safely generated dynamic scripts and hashes for unchanged inline scripts. Both appear in the appropriate source list. Combining them does not remove the requirement for fresh nonces when policies are transmitted, or automatically solve full-page caching.
What happens if the header and script element have different nonces?
This nonce does not authorise the element. Whether enforcement blocks it also depends on other authorisation paths and additional policies. Compare the actual response with its authorised elements and check whether caches or separate generation steps have caused the values to diverge.
Primary sources and our own isolated functional check were examined on 29 September 2026. The described CSP rules are based in part on the CSP3 Working Draft of 16 September 2026. Documentary placeholders, executed local examples and untested production paths are distinguished in the text.
Sources and test limits
- MDN: nonces in CSP
- W3C: nonce requirements
- PCI SSC: payment page security
- Node.js: randomBytes and Web Crypto
- PHP: random_bytes
- Python: secrets
- W3C: strict-dynamic
- W3C: strict CSP
- MDN: script-src and strict-dynamic
- MDN: nonces and hashes
- RFC 9111: no-store
- ICO: British Airways Penalty Notice, paragraphs 1.2 and 1.7
- MDN: script-src