What app.use(helmet()) actually does (and what it misses)
By Seaworthy · July 7, 2026
Photo by Eugenia Pan'kiv on Unsplash
Eleven headers, one line of code
app.use(helmet()) adds eleven HTTP security headers to every response. The Express ecosystem is right to treat it as table stakes. But the defaults are a starting point, not a finished configuration. Several headers you probably want are absent, and one of the included defaults will break your app if you do not tune it.
What the defaults include
Out of the box, helmet sets X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: no-referrer, Strict-Transport-Security, Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Resource-Policy: same-origin, and a Content-Security-Policy with a restrictive default. There are a few others (X-DNS-Prefetch-Control, Origin-Agent-Cluster, etc.) that mostly do their job quietly.
Three of the above deserve scrutiny.
X-Content-Type-Options
nosniff prevents browsers from MIME-sniffing a response away from its declared Content-Type. If you serve user-uploaded files, this matters: without it, a browser might execute an uploaded HTML file even when you serve it as text/plain. The default is correct. Leave it alone.
HSTS and the proxy problem
Strict-Transport-Security with a 180-day max-age is sensible for production. The catch: helmet only sends this header when req.secure is true. Behind nginx, a load balancer, or Cloudflare, req.secure can be false even when the upstream connection was HTTPS. Your production app silently skips the header.
// Add this before helmet() if you're behind a reverse proxy
app.set('trust proxy', 1);
Without it, you may never test that HSTS is actually being sent. Verify with:
curl -sI https://yourapp.com | grep -i strict-transport
CSP will break your app
This is where teams get caught out. Helmet's default Content-Security-Policy includes script-src 'self'. That means inline <script> tags are blocked, inline event handlers (onclick="...") are blocked, eval() is blocked, and anything loaded from a CDN is blocked.
If you have Google Analytics, a third-party chat widget, or a frontend framework that injects inline scripts during hydration, you will see silent failures in the browser console. The page renders, but things just stop working.
The fix is not to disable CSP. Configure it to match what your app actually loads:
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.example.com"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
})
);
If you have inline scripts that are hard to extract, use nonces rather than adding 'unsafe-inline'. Nonces are per-request and still provide meaningful protection. 'unsafe-inline' negates most of what CSP is trying to do.
What helmet does not set
Three headers you almost certainly want are not included in the defaults.
Permissions-Policy controls access to browser APIs: camera, microphone, geolocation, payment. Helmet does not configure this at all. If your app has no business using these, block them explicitly:
app.use((req, res, next) => {
res.setHeader(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=()'
);
next();
});
Cross-Origin-Embedder-Policy (COEP) is off by default because enabling it requires every cross-origin resource your page loads to opt in with Cross-Origin-Resource-Policy: cross-origin. Most third-party scripts and images have not set that header, so enabling COEP breaks them. However, if you need SharedArrayBuffer (WebAssembly threading, high-resolution timers), you need both COEP and Cross-Origin-Opener-Policy: same-origin. Helmet supports it:
helmet({ crossOriginEmbedderPolicy: { policy: 'require-corp' } })
Test this in staging before shipping. It will break things.
Cross-Origin-Resource-Policy is set by helmet to same-origin, which prevents other origins from loading your resources. That is the right default for most apps. If you serve fonts, images, or scripts that other sites legitimately embed, you need to change it to cross-origin.
The actual takeaway
"We have helmet installed" is not the same as "our headers are correctly configured." CSP needs to reflect what your app actually loads. HSTS depends on trust proxy being set. Permissions-Policy requires you to act explicitly. The headers that matter most also require the most deliberate attention.
Audit what you are actually sending:
curl -sI https://yourapp.com | grep -iE 'content-security|strict-transport|permissions-policy|cross-origin'
Compare the output against what you intended to configure. The gap is usually instructive.
Seaworthy flags missing Permissions-Policy headers and overly permissive CSP configurations as part of its security checks.
This article was generated by AI and summarises publicly available sources.
Seaworthy scans your repo for the issues covered in articles like this one.
Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.