Skip to content

What helmet actually sets

Severity: mediumApplies to: helmet 8.xFacts last verified 2026-07-26 against helmet 8.3.0

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
the fix
import helmet from "helmet";
app.use(helmet());
verify it workedRun this in: http response
Terminal window
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, 31536000

The 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.

before you ship this

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.