cheat sheet
helmet
Package-level reference for helmet on npm — default headers, CSP customization, per-route overrides, HSTS, and v7→v8 migration.
helmet
What it is
helmet is the canonical security-header middleware for Express, Connect, and any HTTP server using a similar (req, res, next) pattern. It sets ~15 HTTP response headers — Content-Security-Policy, Strict-Transport-Security, X-Frame-Options, Referrer-Policy, Cross-Origin-Opener-Policy, and more — to safer-than-default values, mitigating clickjacking, XSS, MIME-sniffing, and protocol-downgrade attacks.
It's the default "add this on day one" middleware for any production Node web server. The defaults are conservative and have been tightened across major releases — v7 (2023) and v8 (2024) added new headers like Cross-Origin-*-Policy and removed deprecated ones like Expect-CT.
For Fastify, the equivalent is @fastify/helmet. For Koa, koa-helmet. The underlying header-setting logic is the same.
Install
# npm / pnpm / yarn / bun
npm install helmet
pnpm add helmet
yarn add helmet
bun add helmet
Output: runtime dep. ~10 KB unpacked. TS types bundled.
# For Fastify
npm install @fastify/helmet
# For Koa
npm install koa-helmet
Output: framework-specific bridges. Same header set, framework-appropriate plumbing.
# No CLI; helmet is library-only
Output: helmet is middleware; no binary.
Versioning & Node support
- Current major line is
8.x(stable since 2024 — removed deprecatedexpectCt, refreshed defaults, ESM-first build). The7.xline is still supported. - Node ≥18 required (helmet 8.x). Helmet 7.x supports Node ≥16.
- Pure JS, TS types bundled. Dual ESM + CJS publishing.
- Always a runtime dependency — your server registers the middleware at startup.
- Strict semver — major bumps remove deprecated headers (e.g. v7 dropped
X-XSS-Protection).
Package metadata
- Maintainer: Evan Hahn (
@EvanHahn) + helmet org - Project home: github.com/helmetjs/helmet
- Docs: helmetjs.github.io
- npm: npmjs.com/package/helmet
- License: MIT
- First released: 2012
- Downloads: ~10 million per week
Peer dependencies & extras
Zero runtime dependencies. Framework bridges as separate packages:
| Package | Purpose |
|---|---|
@fastify/helmet | Fastify plugin — same headers, Fastify hooks. |
koa-helmet | Koa middleware bridge. |
nestjs-helmet | NestJS adapter. |
next-helmet | Next.js adapter (for next.config.js headers). |
csp-builder | Programmatic CSP construction — pair with helmet's contentSecurityPolicy. |
Alternatives
| Library | Trade-off |
|---|---|
Manual res.setHeader calls | Free; no dep. Tedious; easy to miss headers. Don't. |
@fastify/helmet | Same logic, Fastify-native. Pick if on Fastify. |
koa-helmet | Same logic, Koa-native. Pick if on Koa. |
| CDN-level headers | Cloudflare Transform Rules, Vercel next.config.js, AWS CloudFront — set headers at the edge. Pick when you want a single config layer. |
| Reverse proxy | nginx/Caddy can set the same headers. Pick for non-Node origins. |
csurf | CSRF token middleware (different problem — helmet doesn't do CSRF). |
Common gotchas
- CSP is the part you must customize. Default CSP is
default-src 'self'— strict. Inline scripts, eval, and third-party CDNs need explicit allowances. Test with browser DevTools' Network → "blocked by CSP" filter. X-XSS-Protectionis gone in v7+ — modern browsers ignore it (or worse, can be tricked by it). If you specifically need it for legacy browsers, set manually.Expect-CTis gone in v8 — header was deprecated in 2023.- HSTS makes HTTPS sticky. Setting
Strict-Transport-Security: max-age=...tells browsers to refuse HTTP for the duration. Don't set on a dev environment served over HTTP — you'll lock yourself out. crossOriginResourcePolicy: "same-origin"breaks CDN-hosted assets. Override to"cross-origin"for the asset routes.- Helmet doesn't set CORS headers. Use
corspackage or a reverse proxy. Helmet only sets security headers, not CORS.
Real-world recipes
Default headers — one-line setup
import express from "express";
import helmet from "helmet";
const app = express();
app.use(helmet());
app.get("/", (_req, res) => res.send("Hello"));
app.listen(3000);
Output: request / and inspect headers:
Content-Security-Policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;...
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=15552000; 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
15 headers from one line. Safe defaults; tighten or relax as needed.
Custom CSP for a real app
import helmet from "helmet";
app.use(helmet({
contentSecurityPolicy: {
useDefaults: true,
directives: {
"default-src": ["'self'"],
"script-src": ["'self'", "https://cdn.example.com", "'sha256-AbCdEf...'"],
"style-src": ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
"img-src": ["'self'", "data:", "https://images.example.com"],
"font-src": ["'self'", "https://fonts.gstatic.com"],
"connect-src": ["'self'", "https://api.example.com", "wss://ws.example.com"],
"frame-src": ["'none'"],
"object-src": ["'none'"],
"upgrade-insecure-requests": [],
},
},
}));
Output: strict CSP with explicit allowances for your CDN, fonts, API, and WebSocket. Each directive lists the allowed sources. Test in DevTools and watch for Refused to load ... because it violates the following Content Security Policy directive warnings.
'sha256-...' allowlists a specific inline script by its SHA-256 hash — safer than 'unsafe-inline'.
Per-route override
import helmet from "helmet";
// Global defaults
app.use(helmet());
// API endpoints — disable framebusting (some clients embed via iframe)
app.use("/api", helmet.frameguard({ action: "deny" }));
// Image-serving route — allow cross-origin
app.get("/images/:id", helmet({
crossOriginResourcePolicy: { policy: "cross-origin" },
}), (req, res) => {
// serve image
});
// Embed-friendly route — drop frame guard
app.get("/embed/:id", (req, res, next) => {
res.removeHeader("X-Frame-Options");
res.removeHeader("Content-Security-Policy");
next();
}, embedHandler);
Output: layered policies. Global default is strict; specific routes relax for legitimate needs.
HSTS — enforce HTTPS
app.use(helmet.hsts({
maxAge: 63072000, // 2 years
includeSubDomains: true,
preload: true, // for hstspreload.org submission
}));
Output:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Production checklist:
- Start with
maxAge: 0and watch logs — make sure you're not breaking subdomains. - Bump to
maxAge: 3600(1 hour) for a week to catch issues. - Bump to
maxAge: 31536000(1 year). - Add
preloadand submit to https://hstspreload.org.
Once preloaded, the entry can take months to remove — be sure.
X-Frame-Options — clickjacking prevention
// Deny all framing
app.use(helmet.frameguard({ action: "deny" }));
// Same-origin only (the default)
app.use(helmet.frameguard({ action: "sameorigin" }));
// Per-route allow specific origin via CSP (X-Frame-Options can't do this)
app.use("/embed", (req, res, next) => {
res.setHeader("Content-Security-Policy", "frame-ancestors https://trusted.example.com");
res.removeHeader("X-Frame-Options");
next();
});
Output: X-Frame-Options: DENY blocks all iframing — strongest clickjacking defence. CSP: frame-ancestors is the modern equivalent and supports allowlisting specific origins.
Disable specific middleware
app.use(helmet({
contentSecurityPolicy: false, // disable CSP entirely
crossOriginEmbedderPolicy: false, // doesn't suit your assets
hsts: false, // not in HTTPS yet
}));
Output: each helmet middleware can be disabled by passing false. Useful for development or for apps that need permissive defaults.
Report-only CSP — test before enforcing
app.use(helmet.contentSecurityPolicy({
reportOnly: true,
directives: {
"default-src": ["'self'"],
"script-src": ["'self'", "https://cdn.example.com"],
"report-uri": ["/csp-report"],
},
}));
app.post("/csp-report", express.json({ type: "application/csp-report" }), (req, res) => {
console.log("CSP violation:", JSON.stringify(req.body));
res.status(204).end();
});
Output: browsers will report violations to /csp-report without blocking. Run for a week, fix violations, then flip to enforcing mode.
Production deployment
CSP development workflow
- Start with
reportOnly: true— collect violations without breaking the app. - Iterate over weeks — fix sources one at a time. Browsers report what's blocked.
- Flip to enforcing — drop
reportOnly. - Layer in nonces or hashes for inline scripts — never use
'unsafe-inline'in production unless absolutely required.
Behind a reverse proxy
Reverse proxies (nginx, Caddy, Cloudflare) can override or strip helmet headers. Audit:
curl -I https://prod.example.com | grep -i content-security
Output:
content-security-policy: default-src 'self'; script-src 'self' 'nonce-abc123'
If the proxy strips Content-Security-Policy, helmet's effort is wasted. Configure the proxy to pass through, or set headers at the proxy and skip helmet.
Sourcemap leakage
Helmet doesn't prevent sourcemap disclosure. Check that production builds don't expose .map files (router.use("/*.map", (_, res) => res.status(404).end())).
Edge runtime compatibility
helmet is Node-only middleware. For Workers/Edge, set headers via the platform — Cloudflare Transform Rules, Vercel headers config, Next.js headers() function.
Performance tuning
Helmet's overhead is negligible — ~10 µs per request, all in setHeader calls. There's no per-request allocation worth tuning.
Cache the CSP string
If your CSP is large and per-request-static, helmet already caches it. If you build a per-request CSP (e.g. nonces), avoid re-stringifying repeatedly:
import crypto from "node:crypto";
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString("base64");
next();
});
app.use((req, res, next) => helmet({
contentSecurityPolicy: {
directives: {
"script-src": ["'self'", `'nonce-${res.locals.nonce}'`],
},
},
})(req, res, next));
Output: per-request nonce — safer than 'unsafe-inline'. Inline scripts must include <script nonce="..."> matching.
Version migration guide
v6 → v7 (2023)
X-XSS-Protectionremoved. Modern browsers ignore it; some are tricked by it.X-Powered-Byremoval moved out — useapp.disable("x-powered-by")directly (Express).- TypeScript types overhaul.
- Stricter defaults for
Cross-Origin-*-Policy.
// v6 — explicit
app.use(helmet.xssFilter());
// v7 — no longer included
// Just don't call it. If you really want X-XSS-Protection, set manually.
v7 → v8 (2024)
- Node ≥18 required (v7 was ≥16).
Expect-CTremoved. Header was deprecated by Chrome in 2023.Origin-Agent-Clusterdefaults to?1— opt-in to origin isolation.- ESM-first build. CJS still works via dual export.
- Refreshed CSP defaults — slightly tighter
default-src.
npm install helmet@^8
Output:
added 1 package in 2s
Migration checks:
# Find any code calling expectCt
rg "helmet\.expectCt|expectCt:" .
# Audit Node version
node --version # must be ≥18
Output:
src/legacy.ts:18: app.use(helmet.expectCt({ maxAge: 30 }));
v20.11.0
If you call helmet.expectCt(...), remove it. Otherwise the upgrade is usually clean.
v8 ongoing
The 8.x line continues to refine defaults. Stay current within ^8.
Security considerations
Helmet itself has had no CVEs — its job is to prevent security issues, not introduce them. The risk is misconfiguration:
- CSP
'unsafe-inline'/'unsafe-eval'defeat XSS protection. Use nonces or hashes instead. - HSTS with
preloadis hard to undo. Once preloaded, the entry can take months to remove. Be sure of your HTTPS posture first. X-Frame-Options: SAMEORIGINdoesn't block cross-origin frames in CSP-aware contexts — useframe-ancestorsin CSP for modern browsers.crossOriginEmbedderPolicy: "require-corp"breaks third-party scripts and iframes. Only enable if you needSharedArrayBuffer(Spectre mitigations).Referrer-Policy: no-referrercan break OAuth flows and analytics. Usestrict-origin-when-cross-originfor a balance.- Helmet doesn't fix XSS — it limits damage. Sanitise user content at the source; helmet is defense-in-depth.
- No CORS. Don't confuse helmet with CORS middleware — use
corsseparately.
Testing & CI integration
import { describe, it, expect } from "vitest";
import express from "express";
import helmet from "helmet";
import request from "supertest";
describe("security headers", () => {
const app = express();
app.use(helmet());
app.get("/", (_req, res) => res.send("ok"));
it("sets X-Frame-Options", async () => {
const res = await request(app).get("/");
expect(res.headers["x-frame-options"]).toBe("SAMEORIGIN");
});
it("sets CSP", async () => {
const res = await request(app).get("/");
expect(res.headers["content-security-policy"]).toContain("default-src 'self'");
});
it("does not set deprecated X-XSS-Protection", async () => {
const res = await request(app).get("/");
expect(res.headers["x-xss-protection"]).toBeUndefined();
});
});
Output:
PASS security headers > sets X-Frame-Options
PASS security headers > sets CSP
PASS security headers > does not set deprecated X-XSS-Protection
For end-to-end CI, scan headers with https://securityheaders.com or curl -I:
curl -I https://prod.example.com | grep -E "Content-Security|Strict-Transport|X-Frame"
Output:
content-security-policy: default-src 'self'
strict-transport-security: max-age=15552000; includeSubDomains
x-frame-options: SAMEORIGIN
Ecosystem integrations
| Tool | Integration |
|---|---|
express | app.use(helmet()) — canonical setup |
@fastify/helmet | Fastify plugin — same headers, Fastify hooks |
koa-helmet | Koa middleware bridge |
nest.js | import { Module } from "@nestjs/common"; app.use(helmet()) |
cors | Pair with helmet — different concerns |
csurf (deprecated) | CSRF tokens — use a modern alt like csrf-csrf |
express-rate-limit | Pair with helmet for DoS resilience |
compression | Order matters — compression first, then helmet |
next.js | Use next.config.js headers() instead of helmet at runtime |
vercel | Use vercel.json headers array, or middleware |
Troubleshooting common errors
- Inline scripts blocked — CSP
script-srcdoesn't include'unsafe-inline', nonce, or hash. Use nonces (preferred) or hashes; never'unsafe-inline'in prod. - Browser refuses to load fonts — CSP
font-srcdoesn't include the font CDN. Addhttps://fonts.gstatic.cometc. - App breaks in iframe (Storybook, MDX preview) —
X-Frame-Options: SAMEORIGIN. Override per-route or disable. - OAuth redirect loses session —
Referrer-Policy: no-referrerbreaks OAuth. Usestrict-origin-when-cross-origin. - HSTS lock-out after enabling without HTTPS — clear browser's HSTS cache (chrome://net-internals/#hsts), or wait
maxAgeseconds. - CDN images blocked —
Cross-Origin-Resource-Policy: same-origin. Override tocross-originfor static asset routes. Cannot find module 'helmet'under ESM — ensure"moduleResolution": "node16"or"bundler"in tsconfig.
When NOT to use this
- You set headers at the edge. Cloudflare Transform Rules, Vercel
headers, nginx — pick one layer, don't double-set. - You're on Next.js / SvelteKit / similar framework. Use the framework's
headers()config — runs at edge, faster than middleware. - You're building an API that's framed/embedded by design (e.g. a widget). Helmet's defaults block this; disable selectively, but pick a different baseline.
- You're on Workers / Edge runtime. Helmet is Node middleware. Set headers via the platform.
- You only need CSP. Roll your own CSP setter; helmet adds 14 other headers you might not want.
See also
- Concept: HTTP — security headers and their effects
- Concept: API — defence-in-depth at API boundaries
- Packages: npm-fastify — pair with
@fastify/helmet