The Helmet Trap: Why Installing a Security Package Isn't Enough
The Helmet Trap: Why Installing a Security Package Isn't Enough
There's a pattern we see constantly in open-source Node.js projects: helmet is in package.json, so developers assume security headers are handled. They're not.
Helmet is the most popular security middleware for Express and Fastify apps. It sets HTTP security headers. But "using helmet" and "being secured by helmet" are two very different things — and the gap between them is where real vulnerabilities live.
Three projects, three different mistakes
Infisical — a secrets management platform (the irony is not lost on us) — uses helmet but disables Content Security Policy:
app.use(helmet({
contentSecurityPolicy: false,
}));
CSP is arguably the most important header helmet sets. It prevents cross-site scripting attacks by controlling which scripts can execute on your pages. Disabling it is like installing a deadbolt and leaving the door open.
Immich — a self-hosted photo management platform — has helmet in its dependencies but disabled by default. The middleware is there in the code, guarded by a configuration flag that defaults to off. Most self-hosters never flip it on because the docs don't emphasize it. The result: zero security headers on a platform that stores your personal photos.
n8n — a workflow automation tool — uses helmet but doesn't configure HSTS (HTTP Strict Transport Security). Without HSTS, browsers will happily connect over plain HTTP on the first visit, making users vulnerable to downgrade attacks and man-in-the-middle interception. For a tool that connects to dozens of third-party services and handles API keys, this matters.
What helmet actually does by default
When you call helmet() with no arguments, you get these headers:
| Header | What it does | Enabled by default? |
|--------|-------------|-------------------|
| Content-Security-Policy | Controls which scripts/styles/images can load | Yes (very restrictive default) |
| Cross-Origin-Embedder-Policy | Controls cross-origin resource embedding | Yes |
| Cross-Origin-Opener-Policy | Isolates browsing context | Yes |
| Cross-Origin-Resource-Policy | Controls cross-origin resource sharing | Yes |
| X-DNS-Prefetch-Control | Controls DNS prefetching | Yes (off) |
| X-Frame-Options | Prevents clickjacking | Yes (SAMEORIGIN) |
| Strict-Transport-Security | Forces HTTPS | Yes (15552000 sec / 180 days) |
| X-Download-Options | Prevents IE from executing downloads | Yes |
| X-Content-Type-Options | Prevents MIME sniffing | Yes (nosniff) |
| X-Permitted-Cross-Domain-Policies | Controls Flash/PDF cross-domain | Yes (none) |
| X-Powered-By | Removes Express version header | Yes (removed) |
| X-XSS-Protection | Legacy XSS filter | Yes (0, disabled — it's actually harmful in modern browsers) |
| Referrer-Policy | Controls referrer information | Yes (no-referrer) |
| Permissions-Policy | Controls browser feature access | No — not set by default |
That's the key insight: helmet's defaults are good, but the moment you start passing options, you're overriding those defaults. And the most common override is contentSecurityPolicy: false.
Why everyone disables CSP
CSP is hard. A strict Content Security Policy will break most applications on the first try. Inline scripts stop working. Third-party analytics scripts get blocked. Fonts from Google CDN won't load. Dynamic style attributes throw violations.
So developers do this:
// "I'll configure CSP properly later" — narrator: they did not
app.use(helmet({
contentSecurityPolicy: false,
}));
And it stays that way forever. The app works, no console errors, ship it.
The problem is that CSP is the one header that actually prevents XSS — the most common web vulnerability. X-Frame-Options prevents clickjacking. X-Content-Type-Options prevents MIME confusion. But CSP is the one that stops an attacker from injecting a <script> tag that sends your users' data to a third-party server.
What helmet does NOT do
Even with perfect configuration, helmet doesn't cover:
- Cookie security flags — You still need to set
Secure,HttpOnly, andSameSiteon your cookies manually (or via your session middleware) - CORS configuration — Helmet doesn't touch CORS. You need the
corspackage or manual header configuration - Rate limiting — No protection against brute force or DDoS
- Input validation — Headers don't protect against SQL injection or command injection
- Authentication — Helmet is not auth middleware
- Permissions-Policy — Not enabled by default. You need to configure it explicitly to restrict browser APIs like camera, microphone, and geolocation
Helmet is one layer. It's the HTTP header layer. If it's your only security measure, you have a single point of partial protection.
The right way to use helmet
Here's a production-ready helmet configuration that doesn't disable the important parts:
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'strict-dynamic'"],
styleSrc: ["'self'", "'unsafe-inline'"], // Required for most CSS-in-JS
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
connectSrc: ["'self'", "https://api.yourdomain.com"],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
},
},
strictTransportSecurity: {
maxAge: 63072000, // 2 years
includeSubDomains: true,
preload: true,
},
}));
Start with a strict policy and loosen it for specific needs — don't start with false and plan to tighten later.
If CSP breaks your app, use the browser's CSP violation reports to find what needs whitelisting:
contentSecurityPolicy: {
directives: {
// ... your directives
reportUri: '/api/csp-report', // Collect violations before enforcing
},
reportOnly: true, // Start in report-only mode
},
Run in reportOnly mode for a week. Collect the violations. Add the legitimate sources to your allowlist. Then switch to enforcement mode.
How this affects audit scores
Our Security Headers & Basics audit has 8 checks in the Security Headers category alone, weighted at 30% of the overall score. The CSP checks (csp-present and csp-no-unsafe-inline) are both high severity (weight: 3 each). HSTS is high severity. These are the checks that separate a B from a D.
Infisical is classified as a mature project in our benchmarks, but disabling CSP costs them on every security-focused audit. You can have excellent code quality, solid authentication, and proper database design — and still fail the headers that protect your users' browsers.
The takeaway
If helmet is in your package.json, open your middleware file and check the options you're passing it. If you see contentSecurityPolicy: false, that's not a temporary workaround — that's a vulnerability you've accepted. If you see helmet imported but behind a feature flag that defaults to off, it's not protecting anyone.
Security headers are the lowest-effort, highest-impact security measure for a web application. They take 20 minutes to configure properly. Helmet makes it even easier. But only if you actually let it do its job.