Helmet & Content Security Policy
helmet() is the easy 80%; a tuned Content Security Policy is the hard, high-value 20% that actually stops XSS.
helmet() with no arguments sets roughly fifteen sensible security headers — X-Content-Type-Options, Strict-Transport-Security, X-Frame-Options and, crucially, a default Content Security Policy. That one line is the best security-per-keystroke in the whole ecosystem.
The header that earns its keep
A tuned CSP is your last line of defense against cross-site scripting: even if an attacker injects a <script>, the browser refuses to run it unless it matches your policy. The trick is that Helmet's default CSP is deliberately strict, so you must declare every legitimate source your app actually uses.
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
}));Roll it out safely
Ship the policy in report-only mode first (reportOnly: true). The browser reports violations without blocking anything, so you discover the CDN or inline snippet you forgot before it breaks real users.
Example
import express from 'express';
import helmet from 'helmet';
import { randomBytes } from 'node:crypto';
const app = express();
// Give every request a fresh CSP nonce.
app.use((req, res, next) => {
res.locals.cspNonce = randomBytes(16).toString('base64');
next();
});
app.use(
helmet({
contentSecurityPolicy: {
// Start in report-only mode in staging, flip to blocking in prod.
reportOnly: process.env.NODE_ENV !== 'production',
directives: {
defaultSrc: ["'self'"],
// Allow ONLY scripts carrying this request's nonce — no unsafe-inline.
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
imgSrc: ["'self'", 'data:', 'https:'],
objectSrc: ["'none'"],
baseUri: ["'self'"],
frameAncestors: ["'none'"], // clickjacking defense
},
},
// HSTS: force HTTPS for a year, only meaningful behind TLS.
strictTransportSecurity: { maxAge: 31_536_000, includeSubDomains: true },
}),
);
app.get('/', (req, res) => {
res.send(`<script nonce="${res.locals.cspNonce}">console.log('trusted')</script>`);
});When to use it
- A developer configures Helmet's Content Security Policy to whitelist only the company's CDN for scripts, blocking any injected third-party payloads.
- A team enables CSP report-only mode to log policy violations to a reporting endpoint before enforcing the policy in production.
- An admin panel restricts frame embedding with the frameAncestors directive to prevent clickjacking from untrusted domains.
More examples
Strict CSP with allowed sources
Configures a strict CSP allowing scripts only from self and a CDN, blocking all frame ancestors to prevent clickjacking.
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'https://cdn.example.com'],
styleSrc: ["'self'", 'https://fonts.googleapis.com'],
imgSrc: ["'self'", 'data:', 'https://img.example.com'],
connectSrc: ["'self'", 'https://api.example.com'],
frameAncestors: ["'none'"],
},
}));CSP report-only mode
Switches Helmet to report-only mode and captures violations at a /csp-violation-report endpoint before enforcing the policy.
app.use(helmet.contentSecurityPolicy({
useDefaults: true,
reportOnly: true, // sends Content-Security-Policy-Report-Only header
directives: {
reportUri: '/csp-violation-report',
},
}));
app.post('/csp-violation-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('CSP violation:', req.body);
res.status(204).send();
});Nonce-based inline script CSP
Generates a per-request nonce, passes it to the CSP directive, and injects it into the HTML so only that inline script is allowed.
const crypto = require('crypto');
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
next();
});
app.use(helmet.contentSecurityPolicy({
directives: {
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
},
}));
app.get('/', (req, res) => {
res.send(`<script nonce="${res.locals.cspNonce}">console.log('allowed');</script>`);
});
Discussion