Secure Headers with Helmet
Browsers enforce a long list of opt-in security behaviors that only activate when the server sends the right response headers. Getting them right by hand is tedious and easy to forget, so the Helmet middleware bundles sane, secure defaults into a single app.use() call. It sets headers like Content-Security-Policy, Strict-Transport-Security, and X-Frame-Options, and removes the fingerprinting X-Powered-By header that Express adds by default. Helmet is not a silver bullet, but it is the cheapest meaningful hardening you can apply to an Express app.
Installing and enabling Helmet
Helmet is a standalone npm package with zero runtime dependencies. Install it and register it as the very first middleware so every response — including errors and static files — is covered.
npm install helmet
const express = require("express");
const helmet = require("helmet");
const app = express();
// Apply all default protections to every response.
app.use(helmet());
app.get("/", (req, res) => {
res.json({ status: "ok" });
});
app.listen(3000);
With the defaults enabled, a plain JSON response now carries a stack of security headers.
Output (response 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=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Order matters: mount
helmet()before your routers and your static-file middleware. Anything that responds before Helmet runs will ship without the headers.
What the key headers do
Each piece of Helmet maps to a header (or a behavior) and can be toggled individually. The table below covers the ones that carry the most weight.
| Header | Helmet module | What it protects against |
|---|---|---|
Content-Security-Policy | contentSecurityPolicy | XSS and data injection by whitelisting valid script/style/asset sources |
Strict-Transport-Security | hsts | Protocol-downgrade and SSL-strip attacks by forcing HTTPS |
X-Frame-Options | frameguard | Clickjacking via embedding your pages in an <iframe> |
X-Content-Type-Options | noSniff | MIME-sniffing that turns uploads into executable content |
Referrer-Policy | referrerPolicy | Leaking full URLs (and query data) to third parties |
Cross-Origin-Resource-Policy | crossOriginResourcePolicy | Side-channel reads of your resources by other origins |
The most important three in practice:
- CSP is the strongest XSS defense available. It tells the browser which origins are allowed to load scripts, styles, images, fonts, and frames. Anything not on the list is blocked, so an injected
<script>from an attacker simply never executes. - HSTS instructs the browser to refuse plain
http://connections to your domain for the configuredmax-age, eliminating the first insecure request after a user types your bare hostname. - X-Frame-Options (and the modern
frame-ancestorsCSP directive) stops other sites from framing yours to trick users into clicking hidden UI.
Customizing the Content-Security-Policy
Helmet’s default CSP is intentionally strict — default-src 'self' blocks every external resource. Real apps usually load assets from a CDN, an analytics provider, or an image bucket, so you override the directives you need while keeping the rest locked down.
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: true, // start from Helmet's safe baseline
directives: {
"default-src": ["'self'"],
"script-src": ["'self'", "https://cdn.example.com"],
"img-src": ["'self'", "data:", "https://images.example.com"],
"connect-src": ["'self'", "https://api.example.com"],
"frame-ancestors": ["'none'"], // disallow all framing
"upgrade-insecure-requests": []
}
}
})
);
useDefaults: true merges your directives over Helmet’s baseline so you only specify what differs. A directive with an empty array (like upgrade-insecure-requests) emits the directive name with no value.
Avoid
'unsafe-inline'inscript-src. It re-enables inline<script>blocks and event-handler attributes — exactly the vector CSP exists to close. Use a nonce or hash instead if you must ship inline scripts.
Tuning individual headers
You can configure or disable any module independently. Pass options to the ones you want to change, and false to switch one off.
app.use(
helmet({
hsts: {
maxAge: 63072000, // two years, in seconds
includeSubDomains: true,
preload: true // eligible for the browser HSTS preload list
},
frameguard: { action: "deny" }, // stronger than the SAMEORIGIN default
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
contentSecurityPolicy: false // turn CSP off entirely (not recommended)
})
);
You can also mount a single piece of Helmet as its own middleware when you want granular control — for example, applying a relaxed CSP only to an embeddable widget route via an Express Router.
const widget = express.Router();
widget.use(helmet.frameguard({ action: "sameorigin" }));
widget.get("/embed", (req, res) => res.send("<div>widget</div>"));
app.use("/widget", widget);
Express 5 changes routing internals but not middleware registration, so every example above works identically on both 4.x and 5.x.
Best Practices
- Mount
helmet()first, before routers and static-file handlers, so no response escapes coverage. - Keep CSP enabled and tighten it iteratively — deploy in report-only mode first if you have a large legacy frontend.
- Never use
'unsafe-inline'or'unsafe-eval'; prefer per-request nonces for genuinely inline scripts. - Only enable HSTS
preloadonce you are certain every subdomain serves HTTPS — preload entries are slow to undo. - Set
frameguardtodeny(or CSPframe-ancestors 'none') unless your pages are meant to be embedded. - Treat Helmet as one layer: pair it with input validation, output escaping, and HTTPS termination rather than relying on headers alone.