Content Security Policy nonces are one of the cleanest ways to shut down a huge class of XSS bugs without rewriting every frontend template you own.
If you’ve ever inherited a server-rendered app with inline scripts sprinkled everywhere, nonces are usually the fastest path to meaningful protection. They let you keep specific inline <script> and <style> blocks while blocking attacker-injected ones.
The short version:
- The server generates a fresh random nonce for every HTTP response
- That nonce goes into the CSP header
- The same nonce is added to trusted inline
<script>or<style>tags - The browser executes only the tags with the matching nonce
If an attacker injects <script>alert(1)</script>, it won’t have the right nonce, so the browser refuses to run it.
Why nonces work
Normally, inline JavaScript is dangerous because any HTML injection can become script execution.
This is the classic problem:
<div>Welcome, {{ username }}</div>
If username is not escaped correctly, an attacker might inject:
<script>fetch('/steal-cookie')</script>
Without CSP, that runs.
With a nonce-based CSP, the browser only allows inline scripts that look like this:
<script nonce="abc123...">console.log('trusted')</script>
And the response header must explicitly allow that nonce:
Content-Security-Policy: script-src 'nonce-abc123...'
No matching nonce, no execution.
That’s the core idea.
What a nonce actually is
A nonce is just a cryptographically random, single-use token. For CSP, it should be:
- Random
- Unpredictable
- Unique per response
Do not reuse the same nonce across requests. If you do, you weaken the policy and make bypasses easier.
A good nonce is generated server-side, usually from secure random bytes, then Base64-encoded.
Example nonce value:
q83v7Q9lXQ2w0v7MZ9dK1A==
Basic CSP nonce header
A minimal CSP using a nonce for scripts looks like this:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-q83v7Q9lXQ2w0v7MZ9dK1A=='; object-src 'none'; base-uri 'self'
That says:
- Default to same-origin resources
- Allow scripts from the same origin
- Allow inline scripts only if they carry the matching nonce
- Block legacy plugin content
- Prevent attackers from changing the
<base>URL
For styles, you can do the same with style-src:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-q83v7Q9lXQ2w0v7MZ9dK1A=='; style-src 'self' 'nonce-q83v7Q9lXQ2w0v7MZ9dK1A=='
A complete Express example
Here’s a small Express app that generates a nonce per request and injects it into both the CSP header and the HTML template.
const express = require('express');
const crypto = require('crypto');
const app = express();
function generateNonce() {
return crypto.randomBytes(16).toString('base64');
}
app.use((req, res, next) => {
res.locals.cspNonce = generateNonce();
res.setHeader(
'Content-Security-Policy',
[
"default-src 'self'",
`script-src 'self' 'nonce-${res.locals.cspNonce}'`,
`style-src 'self' 'nonce-${res.locals.cspNonce}'`,
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'"
].join('; ')
);
next();
});
app.get('/', (req, res) => {
const nonce = res.locals.cspNonce;
res.send(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Nonce demo</title>
<style nonce="${nonce}">
body { font-family: sans-serif; padding: 2rem; }
</style>
</head>
<body>
<h1>CSP nonce demo</h1>
<script nonce="${nonce}">
console.log('This inline script is allowed');
</script>
</body>
</html>
`);
});
app.listen(3000, () => {
console.log('Listening on http://localhost:3000');
});
That’s enough to block arbitrary inline script injection in the page.
If an attacker somehow injects:
<script>alert('xss')</script>
the browser blocks it because there’s no valid nonce.
Using nonces in templating engines
Most real apps render templates, so the pattern is usually:
- Generate nonce in middleware
- Store it in request context or locals
- Add it to the CSP header
- Print it into trusted script/style tags
Example with EJS:
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader(
'Content-Security-Policy',
`default-src 'self'; script-src 'self' 'nonce-${nonce}'; object-src 'none'; base-uri 'self'`
);
next();
});
app.get('/dashboard', (req, res) => {
res.render('dashboard');
});
Template:
<!doctype html>
<html>
<body>
<h1>Dashboard</h1>
<script nonce="<%= nonce %>">
window.appConfig = {
userId: "<%= user.id %>"
};
</script>
<script nonce="<%= nonce %>">
initDashboard();
</script>
</body>
</html>
That works, but be careful: a nonce does not fix unsafe interpolation inside JavaScript.
This is still dangerous if user.id is attacker-controlled and not encoded for JavaScript context:
<script nonce="<%= nonce %>">
window.appConfig = {
userId: "<%= user.id %>"
};
</script>
If your templating engine only does HTML escaping, you can still break out of the JS string. My rule is simple: if I’m embedding data into a script block, I serialize it as JSON.
Safer version:
<script nonce="<%= nonce %>">
window.appConfig = <%- JSON.stringify({ userId: user.id }) %>;
</script>
That’s a big deal. Nonces stop unauthorized script blocks from running. They do not magically sanitize code you wrote yourself.
Nonces for external scripts
A nonce also works on external scripts:
<script nonce="{{nonce}}" src="/static/app.js"></script>
With this CSP:
Content-Security-Policy: script-src 'self' 'nonce-...'
That external script is allowed.
This can be useful when you want a strict policy and only trust script tags you explicitly emitted. It pairs especially well with strict-dynamic, though that changes policy behavior and needs careful rollout.
Example:
Content-Security-Policy: script-src 'nonce-q83v7Q9lXQ2w0v7MZ9dK1A==' 'strict-dynamic'; object-src 'none'; base-uri 'self'
With strict-dynamic, nonce-bearing scripts become the trust root, and host allowlists matter less for descendant loads. That’s powerful, but you should understand the implications before shipping it broadly. For implementation details, https://csp-guide.com is a solid reference alongside official browser documentation.
Common mistakes that break nonce-based CSP
I see the same failures over and over.
1. Reusing the same nonce
Bad:
const nonce = crypto.randomBytes(16).toString('base64');
app.use((req, res, next) => {
res.locals.nonce = nonce;
next();
});
That creates one nonce for the lifetime of the process. Don’t do that.
Good: generate one per response.
2. Generating the nonce in client-side JavaScript
This completely misses the point. The browser needs the nonce in the CSP header before deciding what to execute. The server must generate it.
3. Allowing unsafe-inline anyway
If your policy includes this:
script-src 'self' 'nonce-abc123' 'unsafe-inline'
you’ve undercut the protection. Browsers may treat the policy differently depending on context and compatibility behavior, but as a rule, if you’re deploying nonces for inline script protection, don’t keep unsafe-inline around unless you know exactly why.
4. Injecting the nonce into untrusted markup
Do not expose a reusable template fragment where attacker-controlled HTML can inherit the nonce. If your app lets users store raw HTML and that HTML ends up rendered inside a page where the nonce is present, you need to make sure they cannot create <script nonce="...">.
The nonce is not supposed to be secret forever, but it must only be attached to script tags you trust in that response.
5. Forgetting about event handlers
This won’t work under a strict CSP:
<button onclick="doSomething()">Click me</button>
Nonce-based CSP does not bless inline event handler attributes. Move that code into a script block or external file:
<button id="actionBtn">Click me</button>
<script nonce="{{nonce}}">
document.getElementById('actionBtn').addEventListener('click', doSomething);
</script>
6. Assuming nonces replace output encoding
They don’t. You still need proper escaping, sanitization, and safe DOM APIs.
This is still bad:
element.innerHTML = userSuppliedHtml;
CSP is a mitigation layer, not a permission slip for unsafe rendering.
A practical migration strategy
If I’m adding CSP nonces to an older app, I usually do it in this order:
- Add a report-only CSP first
- Generate a per-response nonce
- Add nonce attributes to existing inline scripts
- Remove inline event handlers like
onclick - Move random script snippets into dedicated blocks or external files
- Enforce the policy once the violations are understood
A report-only header looks like this:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-abc123'; object-src 'none'; base-uri 'self'
That lets you see what would break before you enforce it.
When to use nonces vs hashes
Nonces are best when the HTML is generated dynamically and the server can inject a fresh value into the response.
Hashes are better when the inline content is static and you don’t want per-request header generation.
For most server-rendered apps, I’d pick nonces first. They’re easier to operationalize once you wire them into your template layer.
Final checklist
If you want nonce-based XSS protection to actually hold up:
- Generate the nonce server-side
- Use secure randomness
- Create a new nonce for every response
- Put it in the CSP header
- Add it only to trusted
<script>and<style>tags - Remove
unsafe-inline - Stop using inline event handlers
- Keep doing output encoding and sanitization
- Test in report-only mode before enforcement
Nonces are one of those rare security controls that are both strong and practical. They don’t solve every XSS bug, but they do make exploitation dramatically harder. For a lot of apps, that’s a very good trade.