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

bash
# 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.

bash
# For Fastify
npm install @fastify/helmet

# For Koa
npm install koa-helmet

Output: framework-specific bridges. Same header set, framework-appropriate plumbing.

bash
# 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 deprecated expectCt, refreshed defaults, ESM-first build). The 7.x line 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:

PackagePurpose
@fastify/helmetFastify plugin — same headers, Fastify hooks.
koa-helmetKoa middleware bridge.
nestjs-helmetNestJS adapter.
next-helmetNext.js adapter (for next.config.js headers).
csp-builderProgrammatic CSP construction — pair with helmet's contentSecurityPolicy.

Alternatives

LibraryTrade-off
Manual res.setHeader callsFree; no dep. Tedious; easy to miss headers. Don't.
@fastify/helmetSame logic, Fastify-native. Pick if on Fastify.
koa-helmetSame logic, Koa-native. Pick if on Koa.
CDN-level headersCloudflare Transform Rules, Vercel next.config.js, AWS CloudFront — set headers at the edge. Pick when you want a single config layer.
Reverse proxynginx/Caddy can set the same headers. Pick for non-Node origins.
csurfCSRF token middleware (different problem — helmet doesn't do CSRF).

Common gotchas

  1. 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.
  2. X-XSS-Protection is gone in v7+ — modern browsers ignore it (or worse, can be tricked by it). If you specifically need it for legacy browsers, set manually.
  3. Expect-CT is gone in v8 — header was deprecated in 2023.
  4. 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.
  5. crossOriginResourcePolicy: "same-origin" breaks CDN-hosted assets. Override to "cross-origin" for the asset routes.
  6. Helmet doesn't set CORS headers. Use cors package or a reverse proxy. Helmet only sets security headers, not CORS.

Real-world recipes

Default headers — one-line setup

typescript
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:

text
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

typescript
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

typescript
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

typescript
app.use(helmet.hsts({
  maxAge: 63072000,         // 2 years
  includeSubDomains: true,
  preload: true,             // for hstspreload.org submission
}));

Output:

text
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

Production checklist:

  1. Start with maxAge: 0 and watch logs — make sure you're not breaking subdomains.
  2. Bump to maxAge: 3600 (1 hour) for a week to catch issues.
  3. Bump to maxAge: 31536000 (1 year).
  4. Add preload and submit to https://hstspreload.org.

Once preloaded, the entry can take months to remove — be sure.

X-Frame-Options — clickjacking prevention

typescript
// 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

typescript
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

typescript
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

  1. Start with reportOnly: true — collect violations without breaking the app.
  2. Iterate over weeks — fix sources one at a time. Browsers report what's blocked.
  3. Flip to enforcing — drop reportOnly.
  4. 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:

bash
curl -I https://prod.example.com | grep -i content-security

Output:

text
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:

typescript
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-Protection removed. Modern browsers ignore it; some are tricked by it.
  • X-Powered-By removal moved out — use app.disable("x-powered-by") directly (Express).
  • TypeScript types overhaul.
  • Stricter defaults for Cross-Origin-*-Policy.
typescript
// 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-CT removed. Header was deprecated by Chrome in 2023.
  • Origin-Agent-Cluster defaults to ?1 — opt-in to origin isolation.
  • ESM-first build. CJS still works via dual export.
  • Refreshed CSP defaults — slightly tighter default-src.
bash
npm install helmet@^8

Output:

text
added 1 package in 2s

Migration checks:

bash
# Find any code calling expectCt
rg "helmet\.expectCt|expectCt:" .

# Audit Node version
node --version    # must be ≥18

Output:

text
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:

  1. CSP 'unsafe-inline' / 'unsafe-eval' defeat XSS protection. Use nonces or hashes instead.
  2. HSTS with preload is hard to undo. Once preloaded, the entry can take months to remove. Be sure of your HTTPS posture first.
  3. X-Frame-Options: SAMEORIGIN doesn't block cross-origin frames in CSP-aware contexts — use frame-ancestors in CSP for modern browsers.
  4. crossOriginEmbedderPolicy: "require-corp" breaks third-party scripts and iframes. Only enable if you need SharedArrayBuffer (Spectre mitigations).
  5. Referrer-Policy: no-referrer can break OAuth flows and analytics. Use strict-origin-when-cross-origin for a balance.
  6. Helmet doesn't fix XSS — it limits damage. Sanitise user content at the source; helmet is defense-in-depth.
  7. No CORS. Don't confuse helmet with CORS middleware — use cors separately.

Testing & CI integration

typescript
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:

text
 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:

bash
curl -I https://prod.example.com | grep -E "Content-Security|Strict-Transport|X-Frame"

Output:

text
content-security-policy: default-src 'self'
strict-transport-security: max-age=15552000; includeSubDomains
x-frame-options: SAMEORIGIN

Ecosystem integrations

ToolIntegration
expressapp.use(helmet()) — canonical setup
@fastify/helmetFastify plugin — same headers, Fastify hooks
koa-helmetKoa middleware bridge
nest.jsimport { Module } from "@nestjs/common"; app.use(helmet())
corsPair with helmet — different concerns
csurf (deprecated)CSRF tokens — use a modern alt like csrf-csrf
express-rate-limitPair with helmet for DoS resilience
compressionOrder matters — compression first, then helmet
next.jsUse next.config.js headers() instead of helmet at runtime
vercelUse vercel.json headers array, or middleware

Troubleshooting common errors

  • Inline scripts blocked — CSP script-src doesn't include 'unsafe-inline', nonce, or hash. Use nonces (preferred) or hashes; never 'unsafe-inline' in prod.
  • Browser refuses to load fonts — CSP font-src doesn't include the font CDN. Add https://fonts.gstatic.com etc.
  • App breaks in iframe (Storybook, MDX preview)X-Frame-Options: SAMEORIGIN. Override per-route or disable.
  • OAuth redirect loses sessionReferrer-Policy: no-referrer breaks OAuth. Use strict-origin-when-cross-origin.
  • HSTS lock-out after enabling without HTTPS — clear browser's HSTS cache (chrome://net-internals/#hsts), or wait maxAge seconds.
  • CDN images blockedCross-Origin-Resource-Policy: same-origin. Override to cross-origin for 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