Add a Content Security Policy with Nonces
Ship a restrictive Content Security Policy that blocks inline script injection, uses per-request nonces, and is verified without breaking legitimate assets.
Task
Add a Content-Security-Policy header that reduces XSS impact while allowing the application's own scripts and styles.
When to use this
An application renders user-influenced content and needs defense in depth against script injection. A blanket default-src * policy provides little protection.
Prerequisites
- Control over HTTP response headers
- A way to inject a per-request nonce into server-rendered script tags
- An inventory of third-party script and style origins
Steps
1. Generate a cryptographically random nonce per response and expose it to the template renderer.
import { randomBytes } from 'node:crypto';
app.use((req, res, next) => {
res.locals.cspNonce = randomBytes(16).toString('base64');
next();
});
Expected output: Each response has a unique nonce available to templates.
2. Set a policy that defaults to self, allows scripts only with the nonce, blocks objects and framing, and restricts form and base targets.
app.use((req, res, next) => {
const nonce = res.locals.cspNonce;
res.set('Content-Security-Policy', [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
].join('; '));
next();
});
Expected output: Responses include a restrictive CSP header with the nonce.
3. Add the nonce attribute to every server-rendered script tag. Remove inline event handlers and eval-based code, which the policy blocks.
<script nonce={cspNonce} src="/app.js"></script>
Expected output: Legitimate scripts load; injected inline scripts without the nonce do not.
4. Verify with the browser console and curl. Check for CSP violations and confirm that the page still functions.
curl -sI https://example.com | grep -i content-security-policyExpected output: The header is present and no CSP violations appear in the console.
Troubleshooting
Problem
Inline styles or scripts stop working
Solution
Replace inline handlers with external files or add nonces. Avoid adding 'unsafe-inline' to script-src.
Problem
A third-party widget is blocked
Solution
Add its exact origin to the narrowest directive, not to default-src.
Limitations
- CSP is defense in depth, not a substitute for output encoding and input validation.
- unsafe-inline in style-src weakens protection; migrate to nonces or hashes when feasible.
Start restrictive, then allow
A useful baseline is default-src 'self' plus explicit origins for each resource type. Scripts should use a per-response nonce or hashes; object-src 'none' blocks plugin content; base-uri 'self' prevents base-tag hijacking; frame-ancestors 'none' prevents clickjacking.
res.set('Content-Security-Policy', [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
].join('; '));
Verify
- The header appears on HTML responses.
- The browser console shows no CSP violations for legitimate resources.
- An injected
<script>alert(1)</script>without the nonce does not execute. frame-ancestorsblocks embedding in a test page.- Third-party origins are listed explicitly, not via wildcards.
Rollout
Start with Content-Security-Policy-Report-Only in production, collect violation reports, fix legitimate breakage, then enforce. Keep a reporting endpoint that does not log sensitive URLs or tokens. Review the policy when adding a CDN, analytics script, or font provider.