What helmet actually sets
Express sets no security headers. helmet is how almost everyone adds them, usually
as one line, usually without reading what that line does. Here is the full default
set, read from helmet’s source:
| Header | Default value |
|---|---|
Content-Security-Policy |
a real policy — see below |
Cross-Origin-Opener-Policy |
same-origin |
Cross-Origin-Resource-Policy |
same-origin |
Origin-Agent-Cluster |
?1 |
Referrer-Policy |
no-referrer |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
X-Content-Type-Options |
nosniff |
X-DNS-Prefetch-Control |
off |
X-Download-Options |
noopen |
X-Frame-Options |
SAMEORIGIN |
X-Permitted-Cross-Domain-Policies |
none |
X-XSS-Protection |
0 |
X-Powered-By |
removed |
import helmet from "helmet";
app.use(helmet());curl -sI https://example.com/ | grep -iE 'content-security-policy|strict-transport|x-frame|x-content-type|referrer-policy'X-XSS-Protectionheaderhelmet sets this to 0 — explicitly disabling the legacy browser filter rather than enabling it. Guides recommending 1; mode=block are describing a feature browsers removed because it could be turned against the page it protected.
- accepts
0- default
0- set in
helmet()
X-Content-Type-OptionsheaderSet by default.
- accepts
nosniff- default
nosniff- set in
helmet()
X-Powered-Byheaderhelmet removes the header Express sets.
- default
removed- set in
helmet()
X-Frame-Optionsheaderhelmet sends SAMEORIGIN, not DENY — so by default your own origin may still frame the page. Worth knowing before you decide this control is already handled: if the requirement is that nothing frames the page at all, pass action: "deny". Note the default CSP below also carries frame-ancestors 'self', which is the modern control browsers actually prefer, so tightening only one of the two leaves the other advertising the looser policy.
- accepts
SAMEORIGIN|DENY- default
SAMEORIGIN- set in
helmet()
Content-Security-Policyheaderhelmet ships an enforcing default 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', script-src-attr 'none', style-src 'self' https: 'unsafe-inline', upgrade-insecure-requests. This and the one-year HSTS are the two defaults that break things, and between them they account for most reverts of app.use(helmet()).
- accepts
default-src 'self'|script-src 'self'|object-src 'none'|script-src-attr 'none'|style-src 'self' https: 'unsafe-inline'|upgrade-insecure-requests- default
a real policy, not an empty one- set in
helmet()
Content-Security-Policy received an invalid directive value for "default-src"A whole policy string was pasted into one directive's array, semicolons included. Each directive takes only its own values; the semicolons belong between keys of the directives object, not inside a value.
Read in: helmet — reproduced on helmet 8.3.0Content-Security-Policy received a duplicate directive "default-src"The same directive was given twice under both spellings — `defaultSrc` and `default-src` normalise to one name. Helmet refuses rather than picking a winner.
Read in: helmet — reproduced on helmet 8.3.0Content-Security-Policy needs a default-src but none was provided. If you really want to disable it, set it to `contentSecurityPolicy.dangerouslyDisableDefaultSrc`.Raised when `useDefaults: false` is set and no default-src is supplied. Note what helmet does NOT check: a misspelled directive name is accepted and shipped verbatim, so `scriptSrc2` becomes a `script-src2` directive that every browser ignores, silently.
Read in: helmet — reproduced on helmet 8.3.0Three defaults worth knowing before you ship them
Section titled “Three defaults worth knowing before you ship them”X-XSS-Protection: 0 is deliberate. Helmet sets the header to disable the legacy
browser XSS filter. A large body of “secure your Express app” material still says to
send 1; mode=block; that filter could be manipulated into disabling a page’s
legitimate scripts, and browsers removed it. Helmet is right and the guides are stale.
This is the same conclusion Django reached by
removing its setting in 4.0, and the same one
Spring Security reaches by sending 0 itself.
HSTS is one year, immediately. max-age=31536000; includeSubDomains ships the
moment you add helmet(). A browser that sees it will refuse plain HTTP for this host
and every subdomain for a year, and it will not forget on request. If any
subdomain does not serve HTTPS yet, that is an outage you cannot undo by removing the
header. Ramp it instead:
app.use(helmet({ hsts: { maxAge: 60 } })); // then 3600, 86400, 31536000The default CSP is real and will break most applications. It includes
default-src 'self', script-src 'self', object-src 'none' and
upgrade-insecure-requests. Any CDN script, analytics snippet, or inline <script>
stops executing. This is the single most common reason app.use(helmet()) gets
reverted rather than configured.
Note X-Frame-Options: SAMEORIGIN here, where Django defaults to
DENY — helmet is the more permissive of the two.
Adding helmet() to an existing app changes twelve headers at once, and the CSP and
HSTS are the two that bite.
Turn the CSP off while you fit the rest, then reintroduce it in report-only mode and read the violations before enforcing:
app.use(helmet({ contentSecurityPolicy: false }));If a proxy in front of Express also sets these headers you now have two sources of truth and possibly duplicate values. Decide which layer owns them — that question is on application vs server.
Related
Section titled “Related”- X-Powered-By — which helmet also removes, and why it barely matters
- CORS configuration — the other response-header concern