Content security policy
Start from default-src 'self' and allow only what you load.
helmet.jshelmetjs.github.io
helmet.js
From security-best-practices: Helmet config, OWASP A05.
Build the header
Toggle directives until all ten land as intended.
No CSPBad
(no Content-Security-Policy header)
10/10 load · 3 unwanted
Your CSPGood
import helmet from "helmet";
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://trusted-cdn.com"],
styleSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'", "https:", "data:"],
objectSrc: ["'none'"], // 'self' would still run an uploaded .swf
mediaSrc: ["'self'"],
frameSrc: ["'none'"], // 'self' would still frame an uploaded .html
},
},
}));
Fire an injection
Pick a payload, then edit the policy.
// next.config.js
const csp = [
"default-src 'self'",
"script-src 'self' https://trusted-cdn.com",
"img-src 'self' data: https:",
"connect-src 'self' https://api.example.com",
"object-src 'none'",
"frame-src 'none'",
].join("; ");
module.exports = {
async headers() {
return [{ source: "/(.*)", headers: [{ key: "Content-Security-Policy", value: csp }] }];
},
};
'unsafe-inline' reopens XSS
Type or load an injected comment.
Nonce
'unsafe-inline'Bad
NonceGood
import crypto from "node:crypto";
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString("base64"); // new on every response
next();
});
app.use(helmet({
contentSecurityPolicy: {
directives: {
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`, "https://trusted-cdn.com"],
},
},
}));
// <script nonce="<%= nonce %>">boot()</script>
Why it matters, pros and cons
Why it matters
The browser enforces the allow-list, so one missed escape can't run script.
Pros
- Injected scripts from unknown origins are refused.
connect-srclimits where stolen data goes.- Report-Only mode rolls it out safely.
Cons
- Every new third party needs a policy edit.
'unsafe-inline'undoes most XSS protection; use nonces.- Defense in depth only; escaping output is the fix.