Customizing and Disabling Helmet CSP Directives
This guide is part of the Node.js Express Helmet Configuration reference and covers the exact mechanics of reshaping Helmet’s Content-Security-Policy middleware: overriding individual directives, controlling whether your object merges with or replaces Helmet’s baseline via useDefaults, attaching a different policy per route, switching to report-only, and turning the CSP middleware — or any single Helmet middleware — off entirely. Every configuration here is a runnable Express fragment.
Configuration Syntax & Exact Values
Helmet’s CSP middleware is helmet.contentSecurityPolicy(options). It takes three keys that matter for customization: useDefaults, directives, and reportOnly.
helmet.contentSecurityPolicy({
useDefaults: true, // merge your directives onto Helmet's baseline
directives: {
"script-src": ["'self'", "https://cdn.example.com"],
"img-src": ["'self'", "data:"],
"upgrade-insecure-requests": [], // valueless directive: pass []
},
reportOnly: false, // true => Content-Security-Policy-Report-Only
});
The resulting header string is assembled from directives in insertion order, each directive rendered as name value1 value2; :
Content-Security-Policy: default-src 'self'; base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src 'self' https://cdn.example.com; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; upgrade-insecure-requests
Directive breakdown for the customization keys:
useDefaults: true(the default) — Helmet starts from its built-in baseline policy and applies yourdirectiveson top. A directive you specify replaces the baseline entry of the same name; directives you omit are kept from the baseline.useDefaults: false— Helmet uses only thedirectivesyou provide. Nothing is merged in; the object is the entire policy.- Directive keys accept camelCase (
scriptSrc) or the hyphenated header name ("script-src"). Both emit the same directive. - A value of
[]renders a valueless directive such asupgrade-insecure-requests. - Setting a directive to
nullunderuseDefaults: trueremoves that directive from the merged result — the way to drop a baseline directive without listing every other one. reportOnly: truechanges the header name toContent-Security-Policy-Report-Only; the policy is evaluated and violations reported, but nothing is blocked.
The critical rule is per-directive replacement, not deep merge: if the baseline img-src is 'self' data: and you set imgSrc: ["'self'"], the result is 'self' — your array wins outright, it is not unioned with the baseline values.
Server-Side Configuration
Overriding directives on the global policy
Register helmet once and pass a customized contentSecurityPolicy. This example keeps Helmet’s baseline and layers on a CDN for scripts plus a data: image source.
const express = require("express");
const helmet = require("helmet");
const app = express();
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: true,
directives: {
scriptSrc: ["'self'", "https://cdn.example.com"],
imgSrc: ["'self'", "data:"],
connectSrc: ["'self'", "https://api.example.com"],
objectSrc: null, // drop Helmet's baseline object-src directive
},
},
})
);
useDefaults: true is what lets you specify only the three directives you care about while default-src, base-uri, frame-ancestors, and the rest survive from the baseline. objectSrc: null removes that baseline directive rather than emitting it.
Replacing the policy entirely
For an auditable, self-contained policy, set useDefaults: false and list every directive. Nothing is merged, so the header is exactly what you wrote.
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: false,
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"],
imgSrc: ["'self'", "data:"],
connectSrc: ["'self'"],
objectSrc: ["'none'"],
baseUri: ["'none'"],
frameAncestors: ["'none'"],
upgradeInsecureRequests: [],
},
},
})
);
Per-route CSP
Mount a stricter or looser policy on specific routes by adding a second CSP middleware after the global one. The last Content-Security-Policy header set for a response wins, so a route-level helmet.contentSecurityPolicy(...) overrides the app-wide policy for that path only.
// App-wide baseline for every response.
app.use(helmet());
// A route that embeds a third-party widget needs a looser script-src.
const widgetCsp = helmet.contentSecurityPolicy({
useDefaults: true,
directives: {
scriptSrc: ["'self'", "https://widget.vendor.com"],
frameSrc: ["https://widget.vendor.com"],
},
});
app.get("/embed", widgetCsp, (req, res) => {
res.render("embed");
});
Because helmet.contentSecurityPolicy calls res.setHeader, the route-level middleware replaces the header value set by the global helmet() for /embed. Every other route keeps the baseline.
Disabling a single Helmet middleware
Pass false for any middleware key in the top-level helmet(...) options to turn just that one off while keeping the rest. This is how you ship Helmet without CSP — for example when a reverse proxy or a separate CDN layer owns the policy.
app.use(
helmet({
contentSecurityPolicy: false, // no CSP header emitted by Helmet
// every other Helmet default (HSTS, X-Content-Type-Options,
// X-Frame-Options, Referrer-Policy, etc.) still applies
})
);
Disabling CSP does not touch HSTS, X-Frame-Options, or Referrer-Policy — those middlewares keep their defaults. Only the one key you set to false is removed.
Report-only mode
Set reportOnly: true to evaluate the policy without enforcing it. The header becomes Content-Security-Policy-Report-Only; violations surface in DevTools and are POSTed to any report-to/report-uri endpoint, but nothing is blocked. This is the safe way to trial a tightened policy.
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: true,
directives: {
scriptSrc: ["'self'"],
"report-uri": ["/csp-report"],
},
reportOnly: true, // emits Content-Security-Policy-Report-Only
},
})
);
Diagnostic & Verification Steps
Confirm the emitted header matches your directives exactly.
# Inspect the enforced CSP header
curl -sI http://localhost:3000/ | grep -i 'content-security-policy'
Expected output for the “replacing the policy entirely” config above:
content-security-policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; upgrade-insecure-requests
Verify report-only mode changed the header name, not just the value:
curl -sI http://localhost:3000/ | grep -i 'content-security-policy-report-only'
Expected output:
content-security-policy-report-only: default-src 'self'; ... ; script-src 'self'; report-uri /csp-report
Confirm a disabled CSP emits no CSP header at all, while other Helmet headers remain:
curl -sI http://localhost:3000/ | grep -iE 'content-security-policy|x-content-type-options|strict-transport-security'
Expected output (no CSP line; other headers present):
strict-transport-security: max-age=15552000; includeSubDomains
x-content-type-options: nosniff
Check that a per-route override actually differs from the baseline:
curl -sI http://localhost:3000/ | grep -i 'content-security-policy'
curl -sI http://localhost:3000/embed | grep -i 'content-security-policy'
The /embed line must include https://widget.vendor.com in script-src; the / line must not. In browser DevTools, the Network panel’s Response Headers for each request shows the same distinction, and the Console reports any Refused to load violation against the active policy for that page.
Edge Cases, Security Implications & Safe Rollback
- Replacement, not union. Setting a directive under
useDefaults: trueoverwrites the baseline entry entirely. If you writeimgSrc: ["data:"]intending to adddata:, you have actually dropped'self'and now onlydata:images load. List every value you need —["'self'", "data:"]— because the array is the whole directive. useDefaults: truecan re-add a directive you removed elsewhere. The merge silently restores baseline directives for any name you did not set. To drop a baseline directive, set it tonull; to guarantee the policy is exactly your object, useuseDefaults: false. See what Helmet sets by default for the full baseline list.'unsafe-inline'instyleSrcfrom the baseline. Helmet’s defaultstyle-srcincludes'unsafe-inline'. If you overridescriptSrcfor a strict policy but leavestyleSrcuntouched underuseDefaults: true, inline styles remain permitted. OverridestyleSrcexplicitly, or move to nonces per configuring CSP with Helmet and nonces.- Duplicate CSP from a proxy. If Nginx, Apache, or Cloudflare also injects a CSP, the browser receives two headers and enforces their intersection, often breaking your policy. Own the header in exactly one layer; if the proxy owns it, disable Helmet’s CSP with
contentSecurityPolicy: false.
Safe rollback. Helmet changes are code-level and non-destructive. To back out a tightened policy without redeploying a full revert:
- Add
reportOnly: trueto thecontentSecurityPolicyoptions — violations are reported, nothing blocks, and the site behaves as before. - Or set
contentSecurityPolicy: falseto remove the CSP header entirely while every other Helmet header stays in place. - Verify with
curl -sIthat the header reverts toContent-Security-Policy-Report-Onlyor disappears, then redeploy the corrected directives.
Conclusion
Trial every customized policy in reportOnly: true on staging first, watching DevTools and your report endpoint until legitimate resources stop appearing as violations. Promote the identical directives object to enforcing in production, keeping useDefaults: false for an auditable policy or null overrides when you merge. If a violation slips through, flip back to report-only or set contentSecurityPolicy: false to unblock immediately, fix the directive, and redeploy.
Frequently Asked Questions
Does setting a directive under useDefaults:true add to or replace Helmet’s baseline value?
It replaces it. Directive merging happens per name, not per value: your array becomes the entire directive. If you need Helmet’s baseline values plus your own, list both explicitly — for example imgSrc: ["'self'", "data:", "https://cdn.example.com"] — because nothing is unioned automatically.
How do I remove one of Helmet’s default CSP directives without rewriting the whole policy?
Keep useDefaults: true and set that directive to null. For example objectSrc: null drops the baseline object-src from the merged header while every other default survives. Setting it to [] instead would emit a valueless directive, which is different.
What is the difference between contentSecurityPolicy:false and reportOnly:true?
contentSecurityPolicy: false removes the CSP header entirely — Helmet emits nothing for CSP. reportOnly: true still sends a policy, but as Content-Security-Policy-Report-Only, so violations are reported without being blocked. Use reportOnly to trial a policy and false only when another layer owns the header.
Can different routes have different CSP policies in one Express app?
Yes. Register the global helmet() first, then attach a route-specific helmet.contentSecurityPolicy(...) as middleware on the routes that need it. Because the route-level middleware sets the header after the global one, it replaces the value for those paths while all other routes keep the baseline.
If I disable CSP, do I lose the rest of Helmet’s protection?
No. Passing contentSecurityPolicy: false turns off only the CSP middleware. HSTS, X-Content-Type-Options: nosniff, X-Frame-Options, Referrer-Policy, and the other defaults keep their values. Each Helmet middleware is toggled independently by its own key.