Express¶
Express has no opinion about security headers, so almost everyone reaches for Helmet, which sets a stack of headers including a default CSP. That default is deliberately strict, and it is usually the first thing that breaks an existing app - which is exactly why you want report-only first.
What Helmet gives you¶
- A default
Content-Security-Policyapplied as soon as youapp.use(helmet()), includingdefault-src 'self',script-src 'self',object-src 'none'andupgrade-insecure-requests. - Custom directives passed as a nested
directivesobject. - Directive values that can be functions receiving the request and response, which is how per-request nonces get in.
- A
reportOnlyflag that swaps the header forContent-Security-Policy-Report-Only.
Setting the header¶
Your URLs come from the Setup page:
app.use(
helmet({
contentSecurityPolicy: {
reportOnly: true,
directives: {
"default-src": ["'self'"],
"script-src": ["'self'", "'report-sha256'"],
"report-uri": ["https://abc123.report-uri.com/r/d/csp/reportOnly"],
"report-to": ["default"],
},
},
}),
);
app.use((req, res, next) => {
res.setHeader("Reporting-Endpoints", 'default="https://abc123.report-uri.com/a/d/g"');
res.setHeader("Report-To", JSON.stringify({
group: "default",
max_age: 31536000,
endpoints: [{ url: "https://abc123.report-uri.com/a/d/g" }],
include_subdomains: true,
}));
next();
});
Helmet does not set the two Reporting API headers, so the second middleware does. The Reporting-Endpoints and Report-To headers tell the browser where report-to default sends reports, including the CSP Integrity reports that 'report-sha256' turns on.
Adding a nonce¶
Generate the nonce before Helmet runs, then read it back in a directive function:
import crypto from 'node:crypto'
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64')
next()
})
app.use(
helmet({
contentSecurityPolicy: {
directives: {
"script-src": ["'self'", "'report-sha256'", (req, res) => `'nonce-${res.locals.nonce}'`],
},
},
}),
)
Then render nonce="<%= nonce %>" on your inline scripts.
Things to watch for¶
- Custom
directivesreplace Helmet's defaults for the directives you name, and Helmet merges the rest. Check the header you actually emit rather than assuming your object is additive. - Middleware order matters: the nonce middleware has to run before Helmet, or the directive function reads
undefined. - Helmet's defaults change between major versions. Pin the version and re-check the emitted header after an upgrade, rather than discovering it from production reports.
Official documentation¶
Helmet - Content Security Policy
Start Monitoring with Report URI¶
Already using us for CSP? Find your CSP reporting directives on Setup, or view your CSP reports.
New to Report URI? Create an account and grab your CSP reporting directives from the Setup page.