Content Security Policy: CSP Headers, Nonces, Hashes
Master Content Security Policy: CSP directives, nonces, hashes, reporting, strict-dynamic, nonce-based CSP, hash-based CSP, and production deployment patterns for web security.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Introduction
Content Security Policy (CSP) is a browser security mechanism that restricts what resources a page can load and execute. It is the primary defense against XSS (Cross-Site Scripting) and data injection attacks. CSP tells the browser which sources are trusted for scripts, styles, images, fonts, connections, and frames. Below is a practical guide to CSP directives, nonces, hashes, reporting, strict-dynamic, and production deployment patterns.
How CSP Works
Browser loads page
│
▼
Server sends CSP header
│
▼
Browser enforces policy
│
├── Script from allowed source? → Execute
├── Script from blocked source? → Block + Report
├── Inline script with nonce? → Execute
├── Inline script without nonce? → Block + Report
└── eval() call? → Block (unless 'unsafe-eval')
CSP Directives
| Directive | Controls |
|---|---|
default-src | Fallback for all resource types |
script-src | JavaScript sources |
style-src | CSS sources |
img-src | Image sources |
font-src | Font sources |
connect-src | XHR, fetch, WebSocket, EventSource destinations |
frame-src | iframe sources |
object-src | plugin sources (Flash, Java) |
media-src | audio and video sources |
manifest-src | web manifest sources |
base-uri | allowed <base> tag URIs |
form-action | allowed form submission destinations |
frame-ancestors | who can embed this page in an iframe |
report-uri | where to send violation reports |
upgrade-insecure-requests | auto-upgrade HTTP to HTTPS |
Basic CSP
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline' https://cdn.example.com;
img-src 'self' data: https:;
font-src 'self' https://cdn.example.com;
connect-src 'self' https://api.example.com wss://ws.example.com;
frame-ancestors 'none';
base-uri 'none';
form-action 'self';
upgrade-insecure-requests;
Nonce-Based CSP
Nonces (numbers used once) allow specific inline scripts and styles. The server generates a unique nonce per request and includes it in the CSP header and in the script/style tags.
// middleware/cspNonce.ts — Generate nonce and set CSP header
import crypto from 'crypto';
function cspNonceMiddleware(req: Request, res: Response, next: NextFunction) {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader('Content-Security-Policy', [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' https://cdn.example.com`,
`style-src 'self' 'nonce-${nonce}' https://cdn.example.com`,
`img-src 'self' data: https:`,
`font-src 'self' https://cdn.example.com`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`base-uri 'none'`,
`form-action 'self'`,
`object-src 'none'`,
`upgrade-insecure-requests`,
].join('; '));
next();
}
app.use(cspNonceMiddleware);
<!-- Use nonce in inline scripts -->
<script nonce="<%= nonce %>">
console.log('This inline script is allowed');
</script>
<!-- Use nonce in inline styles -->
<style nonce="<%= nonce %>">
body { margin: 0; }
</style>
Hash-Based CSP
Hashes allow inline scripts by matching their content hash. Use this for static inline scripts that don’t change.
# Generate SHA-256 hash of script content
echo -n "console.log('hello');" | openssl dgst -sha256 -binary | openssl base64 -A
# Output: cSp8NJqrr8wKON2F7LDnMoOJyDDJwKu8s1w...
Content-Security-Policy:
script-src 'self' 'sha256-cSp8NJqrr8wKON2F7LDnMoOJyDDJwKu8s1w...';
<!-- This script's content must match the hash exactly -->
<script>console.log('hello');</script>
Strict-Dynamic
strict-dynamic allows scripts loaded by trusted scripts (nonced or hashed) to load additional scripts without listing them in the CSP. This simplifies CSP for applications with dynamic script loading.
Content-Security-Policy:
script-src 'nonce-abc123' 'strict-dynamic' 'self' https:;
With strict-dynamic:
- Scripts with the nonce execute
- Scripts loaded by nonced scripts also execute (transitive trust)
'self'andhttps:are ignored by browsers that supportstrict-dynamic- Works with module scripts and dynamic imports
<!-- Nonced loader script -->
<script nonce="abc123">
// This can load additional scripts dynamically
const script = document.createElement('script');
script.src = 'https://cdn.example.com/app.js';
document.head.appendChild(script); // Allowed by strict-dynamic
</script>
CSP Reporting
Report-Only mode
Deploy CSP in report-only mode first to find violations without breaking the page:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-abc123';
report-uri /api/csp-reports;
report-to csp-endpoint;
Report endpoint
// routes/cspReports.ts — Receive CSP violation reports
app.post('/api/csp-reports', express.json({ type: 'application/csp-report' }), (req, res) => {
const report = req.body['csp-report'];
console.warn('CSP Violation:', {
documentUri: report['document-uri'],
violatedDirective: report['violated-directive'],
blockedUri: report['blocked-uri'],
lineNumber: report['line-number'],
sourceFile: report['source-file'],
scriptSample: report['script-sample']?.substring(0, 100),
});
// Store for analysis
cspViolationStore.save(report);
res.status(204).end();
});
Report-To header
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://example.com/api/csp-reports"}]}
Content-Security-Policy: ...; report-to csp-endpoint;
Production Deployment
Step 1: Audit current page
# Use Chrome DevTools to find all resource sources
# Lighthouse > Best Practices > CSP
# Or use a CSP generator tool
# Collect all external domains your page loads from:
# - Scripts: <script src="...">
# - Styles: <link rel="stylesheet" href="...">
# - Images: <img src="...">
# - Fonts: @font-face src
# - API calls: fetch() / XMLHttpRequest URLs
# - WebSocket connections
# - iframes
Step 2: Deploy Report-Only
// Start with report-only to find violations
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader('Content-Security-Policy-Report-Only', [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data: https:`,
`font-src 'self' https://cdn.example.com`,
`connect-src 'self' https://api.example.com wss://ws.example.com`,
`frame-ancestors 'none'`,
`base-uri 'none'`,
`form-action 'self'`,
`report-uri /api/csp-reports`,
].join('; '));
next();
});
Step 3: Fix violations
Common violations and fixes:
1. Inline script without nonce
→ Add nonce to <script> tag or move to external file
2. Inline style without nonce
→ Add nonce to <style> tag or move to external file
3. eval() usage
→ Remove eval() or add 'unsafe-eval' (last resort)
4. Script from unlisted domain
→ Add domain to script-src or remove the script
5. WebSocket to unlisted domain
→ Add ws:// or wss:// URL to connect-src
6. Image from data: URI
→ Add data: to img-src
7. Font from CDN
→ Add CDN URL to font-src
Step 4: Enforce CSP
// Switch from Report-Only to enforcing
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}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data: https:`,
`font-src 'self' https://cdn.example.com`,
`connect-src 'self' https://api.example.com wss://ws.example.com`,
`frame-ancestors 'none'`,
`base-uri 'none'`,
`form-action 'self'`,
`object-src 'none'`,
`upgrade-insecure-requests`,
].join('; '));
next();
});
Framework Integration
Next.js
// next.config.js
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'none';
form-action 'self';
`;
module.exports = {
async headers() {
return [{
source: '/(.*)',
headers: [{
key: 'Content-Security-Policy',
value: cspHeader.replace(/\s+/g, ' ').trim(),
}],
}];
},
};
Express with helmet
import helmet from 'helmet';
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`, "'strict-dynamic'"],
styleSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
imgSrc: ["'self'", 'data:', 'https:'],
fontSrc: ["'self'", 'https://cdn.example.com'],
connectSrc: ["'self'", 'https://api.example.com', 'wss://ws.example.com'],
frameAncestors: ["'none'"],
baseUri: ["'none'"],
formAction: ["'self'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
}));
Best Practices
-
For a deeper guide, see CORS Security: Origins, Headers, Preflight, Credentials.
-
Start with Report-Only mode — find violations before enforcing
-
Use nonces for inline scripts — more flexible than hashes for dynamic content
-
Use
strict-dynamic— reduces the need to list every script source -
Set
frame-ancestors: 'none'— prevents clickjacking -
Set
base-uri: 'none'— prevents<base>tag injection -
Set
object-src: 'none'— blocks Flash and other plugins -
Set
form-action: 'self'— prevents form submission to external sites -
Enable
upgrade-insecure-requests— auto-upgrades HTTP to HTTPS -
Monitor CSP reports — fix violations promptly
-
Don’t use
'unsafe-inline'for script-src — defeats the purpose of CSP -
Avoid
'unsafe-eval'— needed for some frameworks but weakens CSP -
Keep CSP as strict as possible — every relaxation is an attack surface
Common Mistakes
- Using
'unsafe-inline'for script-src: allows any inline script, defeating CSP’s XSS protection. Use nonces or hashes instead. - Deploying CSP in enforce mode first: breaks the page for users. Always start with Report-Only.
- Not monitoring CSP reports: violations go unnoticed. Set up logging and alerting.
- Listing too many domains in script-src: increases attack surface. Use
strict-dynamicwith nonces instead. - Forgetting connect-src: fetch/XHR to APIs gets blocked. List all API endpoints.
- Not setting frame-ancestors: page can be embedded in iframes, enabling clickjacking.
FAQ
What is Content Security Policy?
A browser security header that restricts what resources a page can load and execute. It prevents XSS by blocking inline scripts without nonces, scripts from untrusted domains, and other injection attacks.
What is a CSP nonce?
A random string generated per request, included in the CSP header and in <script> / <style> tags. Only scripts with the correct nonce execute. Nonces prevent attackers from injecting scripts since they can’t predict the nonce.
What is strict-dynamic?
A CSP keyword that allows scripts loaded by trusted scripts (nonced or hashed) to load additional scripts. This means you don’t need to list every script source in the CSP. The trust propagates from the nonced loader to its dependencies.
What is the difference between nonce and hash?
Nonces are per-request random values — good for dynamic content. Hashes are content-based — good for static inline scripts that never change. Nonces require server-side generation per request. Hashes can be computed at build time.
How do I handle third-party scripts with CSP?
Add their domain to script-src, or load them via a nonced loader script with strict-dynamic. For analytics scripts that use inline code, add the script’s hash to script-src or load it through a nonced wrapper.
Related Resources
CORS Security: Origins, Headers, Preflight, Credentials
Master CORS security: same-origin policy, CORS headers, preflight requests, credential handling, common misconfigurations, and production security patterns for web APIs.
GuideOAuth2 and OIDC: Authorization Code, PKCE, Token Validation
Master OAuth2 and OpenID Connect for production: authorization code flow with PKCE, token validation, refresh tokens, scopes, JWT verification, and security best practices.
RecipeConfigure HTTP Security Headers with Helmet in Node.js
Set security HTTP headers in Express apps with Helmet — CSP, HSTS, X-Frame-Options, X-Content-Type-Options, and CORS for OWASP-compliant web security
GuideComplete Guide to Web Security Headers
Implement CSP, HSTS, X-Frame-Options, and secure headers. Covers content security policy, CORS, referrer policy, permissions policy, and testing with security scanners.
GuideEncryption at Rest: AES-256, KMS, Envelope Encryption
Master encryption at rest: AES-256-GCM, key management services, envelope encryption, key rotation, database encryption, field-level encryption, and production security patterns.