ject scripts into the same origin. Without a deterministic allowlist, the browser becomes an untrusted runtime. Strict headers transform it into a verified execution boundary.
Core Solution
Implementing CSP and security headers requires a phased, build-integrated approach. The goal is to move from observation to enforcement without breaking user workflows.
Step 1: Audit and Instrument Execution Contexts
Identify all inline scripts, styles, and dynamic attribute assignments. Modern bundlers can extract these during build time. Use csp-ast or helmet-compatible parsers to map execution points.
Step 2: Generate Cryptographic Bindings
For dynamic content, nonces are preferred over hashes. Nonces rotate per request, preventing replay attacks while allowing inline execution. Hashes are static and break when content changes.
import { randomBytes } from 'crypto';
import type { Request, Response, NextFunction } from 'express';
export const generateNonce = (_req: Request, res: Response, next: NextFunction) => {
res.locals.cspNonce = randomBytes(16).toString('base64');
next();
};
Inject the nonce into your template or frameworkâs head injection point:
<script nonce="{{res.locals.cspNonce}}">/* inline logic */</script>
<style nonce="{{res.locals.cspNonce}}">/* inline styles */</style>
Build the policy programmatically to avoid syntax errors and enable environment-specific overrides.
export const buildCSP = (nonce: string, env: 'development' | 'production') => {
const base = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' https://trusted.cdn.com`,
`style-src 'self' 'nonce-${nonce}' https://fonts.googleapis.com`,
`img-src 'self' data: https:`,
`font-src 'self' https://fonts.gstatic.com`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`upgrade-insecure-requests`,
];
if (env === 'development') {
base.push(`script-src-elem 'self' 'unsafe-eval'`);
}
return base.join('; ');
};
CSP is ineffective in isolation. Pair it with headers that enforce transport security, origin isolation, and capability restriction.
export const securityHeaders = (csp: string) => ({
'Content-Security-Policy': csp,
'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'camera=(), microphone=(), geolocation=(), payment=(self)',
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
});
Step 5: Integrate into Request Lifecycle
Apply headers at the edge or application layer. Prefer edge deployment for cacheability and reduced compute load.
import express from 'express';
import { generateNonce, buildCSP, securityHeaders } from './security';
const app = express();
app.use(generateNonce);
app.use((req, res, next) => {
const nonce = res.locals.cspNonce;
const csp = buildCSP(nonce, process.env.NODE_ENV as 'development' | 'production');
Object.entries(securityHeaders(csp)).forEach(([key, value]) => {
res.setHeader(key, value);
});
next();
});
Architecture Decisions & Rationale
- Nonces over hashes: Nonces support dynamic inline execution without rebuilding assets. Hashes require static content and break with framework hydration or SSR streaming.
report-to over report-uri: report-to integrates with the modern Reporting API, supports structured JSON, batch delivery, and automatic retry. report-uri is deprecated and lacks delivery guarantees.
- Edge vs Application layer: Deploy CSP at the CDN/reverse proxy level to avoid per-request header computation in the app server. Use application-level generation only when nonces must be bound to request context.
- Gradual enforcement: Start with
Content-Security-Policy-Report-Only to capture violations without blocking execution. Transition to enforce mode only after report endpoints stabilize and false positives are eliminated.
- Third-party dependency mapping: Maintain a registry of external script origins. Use
script-src allowlists scoped to specific CDNs. Avoid wildcard https: unless absolutely necessary.
Pitfall Guide
-
Using unsafe-inline as a fallback
unsafe-inline defeats the entire purpose of CSP. It allows any inline script to execute, nullifying nonce/hash protections. Modern browsers ignore unsafe-inline when a valid nonce or hash is present, but legacy browsers still honor it. Remove it immediately and refactor inline logic to external modules or use nonces.
-
Ignoring base-uri and form-action
Attackers can redirect form submissions or base URL resolution to malicious origins. base-uri 'self' prevents <base> tag manipulation. form-action 'self' https://trusted-payment.com restricts where forms can POST. Omitting these leaves authentication and data submission vectors exposed.
-
Misconfiguring report endpoints
Sending CSP violations to unauthenticated or public endpoints exposes internal URLs, user agent strings, and potentially sensitive DOM states. Use authenticated reporting endpoints with rate limiting, IP allowlisting, and payload sanitization. Rotate endpoint URLs periodically.
-
Overlooking third-party script injection
Analytics, chat widgets, and ad networks often inject scripts via document.write or dynamic DOM insertion. If these origins arenât in script-src, CSP will block them silently in enforce mode. Audit all third-party dependencies, request CSP-compatible delivery methods, and isolate high-risk widgets in sandboxes or cross-origin iframes.
-
Treating CSP as input validation
CSP is a runtime execution boundary, not a substitute for output encoding or parameterized queries. It mitigates exploitation of injected scripts but does not prevent SQL injection, SSRF, or logic flaws. Maintain defense-in-depth: validate, encode, and sanitize at the application layer; enforce execution boundaries at the browser layer.
-
Deploying Cross-Origin-Embedder-Policy: require-corp without resource alignment
require-corp blocks cross-origin resources that lack Cross-Origin-Resource-Policy: same-site or explicit CORS headers. This breaks legacy CDNs, image hosts, and unconfigured APIs. Test thoroughly in staging, and use credentialless as a transitional value if full isolation isnât feasible.
-
Skipping CI/CD header validation
Headers deployed manually drift over time. Integrate CSP syntax validation into your pipeline using csp-validator or helmet-compatible linters. Fail builds on malformed policies, missing nonces, or deprecated directives. Automate report endpoint health checks to detect silent delivery failures.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Legacy monolith with heavy inline scripts | Nonce-based CSP + gradual migration to external modules | Preserves functionality while enabling enforcement | Medium (refactor time) |
| Micro-frontend architecture with shared CDNs | Strict CSP + COOP/COEP + resource origin registry | Isolates execution contexts and prevents cross-app pollution | Low (configuration overhead) |
| High-traffic e-commerce with third-party payment widgets | Sandboxed iframes + frame-ancestors 'none' + strict form-action | Limits blast radius of widget compromise | Low (architectural adjustment) |
| Internal enterprise dashboard | Full header suite + Permissions-Policy lockdown | Minimizes attack surface for authenticated users | Negligible |
| Public marketing site with analytics/ad networks | Permissive report-only â strict enforce after 30-day audit | Balances marketing flexibility with security maturity | Low (monitoring cost) |
Configuration Template
Express Middleware (TypeScript)
import { randomBytes } from 'crypto';
import type { Request, Response, NextFunction } from 'express';
const NONCE_LENGTH = 16;
export const securityHeadersMiddleware = (env: 'development' | 'production') => {
return (req: Request, res: Response, next: NextFunction) => {
const nonce = randomBytes(NONCE_LENGTH).toString('base64');
res.locals.cspNonce = nonce;
const cspDirectives = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' https://cdn.trusted.com`,
`style-src 'self' 'nonce-${nonce}' https://fonts.googleapis.com`,
`img-src 'self' data: https:`,
`font-src 'self' https://fonts.gstatic.com`,
`connect-src 'self' https://api.trusted.com`,
`frame-ancestors 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`upgrade-insecure-requests`,
env === 'development' ? `script-src-elem 'self' 'unsafe-eval'` : '',
].filter(Boolean).join('; ');
res.setHeader('Content-Security-Policy', cspDirectives);
res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=(self)');
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'credentialless');
res.setHeader('Report-To', JSON.stringify({
group: 'csp-endpoint',
max_age: 86400,
endpoints: [{ url: 'https://reports.example.com/csp' }]
}));
next();
};
};
Nginx Edge Deployment
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$request_id'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "credentialless" always;
Quick Start Guide
- Install
helmet or implement a custom middleware that generates a 16-byte Base64 nonce per request and attaches it to res.locals
- Update your HTML/template engine to inject
nonce="{{nonce}}" into all inline <script> and <style> tags
- Deploy a
Content-Security-Policy-Report-Only header with a report-to endpoint pointing to an authenticated logging service
- Monitor violation reports for 7-14 days, update allowlists for legitimate third-party origins, and remove false positives
- Switch to
Content-Security-Policy enforce mode and verify zero blocking errors in production telemetry