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'Three 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