helmet Security Headers
Browsers enforce a layer of security based on the HTTP response headers your server sends — but Express sets almost none of them by default. helmet closes that gap with a single line of middleware that applies a sensible set of protective headers, defending against cross-site scripting, clickjacking, MIME-type sniffing, and information leakage. It is a collection of smaller middlewares bundled together, each toggling one header, so you can keep the safe defaults or selectively customize and disable individual protections. This page covers the defaults, the headers they set, tuning the Content-Security-Policy, and turning specific protections off when they get in the way.
Installing and enabling helmet
helmet is a standalone package with no dependencies. Install it and register it with app.use() near the top of the middleware stack, before your routers, so every response that follows carries the headers.
npm install helmet
const express = require("express");
const helmet = require("helmet");
const app = express();
// Applies all of helmet's default protections to every response.
app.use(helmet());
app.get("/", (req, res) => {
res.send("Hello, secure world");
});
app.listen(3000, () => console.log("Listening on http://localhost:3000"));
Calling helmet() with no arguments enables every default middleware. A response now includes a stack of security headers it would otherwise lack.
Output:
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
X-XSS-Protection: 0
X-DNS-Prefetch-Control: off
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
What the default headers do
Each header addresses a specific class of attack. Understanding what they protect against helps you decide which ones to keep, tighten, or relax.
| Header | Protects against | Default value |
|---|---|---|
Content-Security-Policy | XSS, injection, untrusted resource loading | default-src 'self' plus directive set |
Strict-Transport-Security | Protocol downgrade / SSL stripping | max-age=31536000; includeSubDomains |
X-Content-Type-Options | MIME-type sniffing | nosniff |
X-Frame-Options | Clickjacking via framing | SAMEORIGIN |
Referrer-Policy | Leaking URLs to other sites | no-referrer |
Cross-Origin-Resource-Policy | Cross-origin resource theft | same-origin |
X-XSS-Protection | Legacy XSS auditor (now disabled) | 0 |
Origin-Agent-Cluster | Process-level origin isolation | ?1 |
X-XSS-Protectionis deliberately set to0. The old browser XSS auditor it controlled caused more vulnerabilities than it fixed and is gone from modern browsers — a strong Content-Security-Policy is the real defense.
Customizing the Content-Security-Policy
The CSP is the most powerful and the most likely to need tuning, because the default default-src 'self' blocks every external script, style, and font. Pass a contentSecurityPolicy option with your own directives to allow the origins your app legitimately uses. Directives you specify are merged over helmet’s defaults — pass useDefaults: false to start from an empty policy instead.
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: true,
directives: {
// Allow scripts from self plus a trusted CDN.
"script-src": ["'self'", "https://cdn.jsdelivr.net"],
"style-src": ["'self'", "'unsafe-inline'"],
"img-src": ["'self'", "data:", "https://images.example.com"],
"connect-src": ["'self'", "https://api.example.com"],
// Block this site from being framed entirely.
"frame-ancestors": ["'none'"],
},
},
})
);
You can also configure CSP in report-only mode while you roll it out. The browser reports violations to an endpoint without actually blocking anything, so you can catch missing directives before they break the page.
app.use(
helmet({
contentSecurityPolicy: {
reportOnly: true,
directives: {
"default-src": ["'self'"],
"report-uri": ["/csp-report"],
},
},
})
);
This sends a Content-Security-Policy-Report-Only header instead of the enforcing one.
Disabling specific protections
Some defaults conflict with legitimate use cases — for example, Cross-Origin-Resource-Policy: same-origin blocks a CDN from serving your images to other origins, and a strict CSP can break a third-party embed. Set the corresponding option to false to drop just that one middleware while keeping the rest.
app.use(
helmet({
// Disable CSP entirely (e.g. while debugging, or when a CDN sets it).
contentSecurityPolicy: false,
// Allow this app's resources to be loaded cross-origin.
crossOriginResourcePolicy: false,
// Permit framing from anywhere by removing X-Frame-Options.
frameguard: false,
})
);
You can also configure a single middleware on its own without the full bundle, which is useful when you only want one header or want different settings per route.
// HSTS only, with a longer max-age and preload flag.
app.use(
helmet.hsts({
maxAge: 63072000, // two years
includeSubDomains: true,
preload: true,
})
);
// Stricter frameguard on a sensitive router.
const adminRouter = express.Router();
adminRouter.use(helmet.frameguard({ action: "deny" }));
Only send
Strict-Transport-Securityover HTTPS. If you set HSTS on a site served over plain HTTP — or setpreloadbefore you can guarantee HTTPS on every subdomain forever — you can lock users out of your domain in their browser cache.
This API is identical on Express 4.x and 5.x; helmet’s middleware contract did not change, and the same app.use(helmet()) call works on both.
Best Practices
- Register
helmet()before your routers and static middleware so every response is covered, including error responses. - Keep the defaults on and only relax individual headers when a real feature requires it — disabling the whole bundle throws away free protection.
- Treat the Content-Security-Policy as the primary XSS defense; tune its directives to the exact origins your app loads from rather than using broad wildcards.
- Roll out a new or tightened CSP in
reportOnlymode first, watch the violation reports, then switch to enforcing. - Avoid
'unsafe-inline'and'unsafe-eval'inscript-srcwhere possible — they reopen the injection holes CSP exists to close. - Only enable HSTS (and especially
preload) once HTTPS is guaranteed across the domain and all subdomains. - Set helmet behind your TLS terminator and trust the proxy (
app.set("trust proxy", 1)) so protocol-sensitive headers behave correctly.