Express gives you a router and a middleware chain, and almost nothing else, so every security property of an Express application is something a team chose to add, configure and keep in order. The defaults are deliberately minimal: no security headers, a session cookie that is not marked Secure, no rate limiting, a fingerprinting header and an in-memory session store that the maintainers say is not for production. This guide turns the official Express production security guidance, the Helmet and express-session documentation and the OWASP Node.js cheat sheet into a hardening baseline for Express 4 and 5. It is written for Node.js developers, application security engineers and reviewers who own or assess Express services, and it goes deeper on Express specifics than the general Node.js Security Guide.
Key takeaways
- Middleware order is the security model. Helmet, body limits, rate limiting, session and authentication must run before the routes they protect, and the error handler must be last.
- express-session defaults need changing. The cookie is HttpOnly but not Secure, SameSite is off, the name is the fingerprintable connect.sid and MemoryStore is not designed for production.
- trust proxy decides who your users are. req.ip, req.protocol and req.hostname come from X-Forwarded-* headers once it is enabled, so a wrong value breaks rate limiting, Secure cookies and audit logs.
- Helmet is a baseline, not a policy. It sets 13 headers with sensible defaults, but the Content Security Policy still has to be written for your application and tested in report-only mode first.
- Stay on a patched major version. Express 3.x has had no fixes since 2015, and recent 4.x releases fixed open redirect and XSS flaws in res.redirect and res.location, so version currency is a control in itself.
How identity and access work in Express
Express has no built-in user model. Identity arrives through middleware you add: a session layer, an authentication strategy library or a token verifier, followed by your own authorisation checks. That makes the request pipeline the place where identity can be lost, spoofed or skipped.
Sessions: express-session versus cookie-session
The Express best practices page describes two session modules. express-session stores session data on the server and puts only a signed session ID in the cookie. cookie-session serialises the whole session into the cookie, which the Express team says suits only small sessions of primitive values, kept under 4093 bytes per domain, and only where the client may see the data.
| Option (express-session) | Default | Production setting and why |
|---|---|---|
secret | Required, no default | At least 32 bytes of entropy from a secrets store, not the repository. Pass an array to rotate: the first element signs, all are checked. |
name | connect.sid | A generic name such as sid, ideally with the __Host- prefix (ASVS 5.0 V3.3.3) when the cookie is host-only and Secure. |
cookie.secure | false | true on HTTPS sites. Behind a proxy this also needs trust proxy, or the cookie is never sent. |
cookie.httpOnly | true | Keep. Script access to session IDs is what XSS-driven session theft relies on. |
cookie.sameSite | Not set | 'lax' as a floor, 'strict' where cross-site navigation into authenticated pages is not needed. |
cookie.maxAge | null (browser session) | An absolute lifetime that matches your session policy; combine with rolling: true for idle timeouts. |
resave / saveUninitialized | true (both deprecated defaults) | false unless the store requires otherwise. saveUninitialized: false avoids setting cookies before login or consent. |
store | MemoryStore | A production store (Redis, a database). MemoryStore leaks memory and does not scale past one process. |
Call req.session.regenerate() after successful login so the pre-authentication session ID is discarded, which is the documented defence against fixation (see the Session Fixation Guide). At logout, clear the user, save and regenerate, or call req.session.destroy(), and make sure the server-side record is removed, not just the cookie.
Authentication and authorisation middleware
- Mount order: authentication middleware must be mounted before the routers it protects. A router mounted above
app.use(requireAuth)is public, and this is the most common Express access control bug found in code review. - Deny by default: apply an authentication guard at the router level and explicitly mark the few public routes, rather than adding a guard to each private route and hoping none is missed.
- Object-level checks: a valid session says who the caller is, not what they may touch. Check ownership or tenancy for every
:idparameter in the handler or a shared policy function. - Token APIs: for bearer tokens, verify signature, issuer, audience and expiry in middleware and pin the accepted algorithms. See the JWT Security Guide.
- Express 5 async handling: rejected promises from async handlers now go to the error handler. In Express 4 an unawaited rejection inside an auth check can leave a request hanging or skip the check, so wrap async middleware or upgrade.
Admin roles and privileged access
Express apps usually grow an admin surface: an /admin router, a health or metrics endpoint, a debug route someone added during an incident. These are privileged functions and deserve stronger controls than the rest of the app.
- Separate the router: mount admin routes on their own router with a role check applied once at the top, and consider serving them on a separate hostname or internal-only listener so the same-origin policy and network controls also apply (ASVS 5.0 V3.5.4).
- Step-up authentication: require a recent strong authentication for destructive admin actions, and record the session's authentication time to enforce it.
- Use state-changing methods correctly: sensitive actions on POST, PUT, PATCH or DELETE only (ASVS 5.0 V3.5.3), with CSRF protection for cookie-authenticated forms. The OWASP Node.js cheat sheet notes that
csurfis deprecated; see the CSRF Prevention Guide for current patterns. - No debug in production: remove or gate debug and documentation routes (ASVS 5.0 V13.4.2, V13.4.5), and set
NODE_ENV=productionso Express and template engines stop returning verbose errors. - Static files: Express 5 ignores dotfiles and hidden directories in
express.static()andres.sendFile()by default. On Express 4, setdotfiles: 'ignore'or'deny'and never serve the project root, which exposes.gitand.env(ASVS 5.0 V13.4.1).
Integrations, API tokens and service accounts
Most Express services hold machine credentials: database passwords, cloud keys, payment and email API keys, the session signing secret and outbound OAuth client secrets. These non-human identities are often more powerful than any user session.
- Secrets out of code: load the session secret and API keys from a secrets manager or injected environment at start-up, never from a committed
.env. Rotate with the array form ofsecretso users stay logged in. See the Secrets Management Guide. - Least privilege per integration: give each downstream key the narrowest scope the service needs, and separate read and write credentials where the provider allows. See the API Key Management Guide.
- Inbound service tokens: for service-to-service calls into the app, prefer mTLS or short-lived signed tokens over static shared keys, and verify them in dedicated middleware (ASVS 5.0 V13.2.1).
- Outbound allowlists: if a route fetches a URL supplied by a user or a webhook, restrict destinations to an allowlist (ASVS 5.0 V13.2.4, V13.2.5) to prevent the app's network identity being used for SSRF.
- Dependencies are integrations too: an Express app typically pulls hundreds of transitive packages that run with the same access to those secrets. Use
npm cifrom a lockfile, runnpm auditin CI and read the Malicious Package and Dependency Confusion Guide.
Logging and detection
Express logs nothing by default. Add structured request logging (for example a JSON logger middleware) and application events, and make sure the client IP in those logs is the real one.
Get trust proxy right first
The Express documentation sets trust proxy to false by default, so req.ip is the socket address. Behind a load balancer every request then appears to come from the proxy, and a rate limiter keyed on IP will throttle everyone at once. Setting it to true trusts the left-most X-Forwarded-For entry, which a client can forge unless the last proxy overwrites the header. The safer choices are a hop count that matches a single fixed path (for example 1) or the proxy's addresses or subnets ('loopback', 'uniquelocal' or explicit CIDRs). ASVS 5.0 V15.3.4 requires that proxies pass the original IP in fields that cannot be manipulated.
What to log and alert on
| Event | Where to capture it | Detection use |
|---|---|---|
| Login success and failure, with username and client IP | Authentication handler | Credential stuffing (ATT&CK T1110.004) and brute force (T1110) |
| Rate limit rejections (HTTP 429) | Limiter handler callback | Abuse sources, and misconfigured trust proxy if one IP dominates |
| Session regenerate, destroy and privilege change | Session and admin code | Session replay after logout, use of stolen cookies (T1539) |
| Authorisation denials (HTTP 403) by route and user | Policy middleware | Object enumeration and horizontal privilege attempts |
| Unhandled errors and HTTP 500 rate | Final error handler | Exploit attempts against parsers and handlers (T1190) |
| CSP violation reports | Report endpoint configured in the policy (ASVS 5.0 V3.4.7) | Injected scripts and policy gaps |
Never log session IDs, bearer tokens, the Authorization header or request bodies from login and payment routes. See the JWT and Session Tokens in Code and Logs Guide and the Secure Error Handling and Logging Guide.
Metrics that show the controls work
- Header coverage: percentage of production routes returning CSP, HSTS, X-Content-Type-Options and COOP, checked by an automated scan.
- Cookie compliance: share of session and auth cookies with Secure, HttpOnly, SameSite and a
__Host-prefix. - Patch latency: days from an Express, Node.js or middleware advisory to deployment, against your documented remediation window (ASVS 5.0 V15.1.1).
- Login abuse blocked: rate limit and lockout events per day on authentication routes, trended against successful logins.
- Unprotected routes: count of routes reachable without authentication that are not on the documented public list, from a route inventory test.
Hardening baseline
Recommended middleware order
app.disable('x-powered-by')andapp.set('trust proxy', ...)matched to your real proxy path.helmet()with an application-specific Content Security Policy.- A general rate limiter, plus a stricter one on login, password reset and token endpoints.
- Body parsers with explicit limits, for example
express.json({ limit: '100kb' }), sized to the largest legitimate payload. The OWASP cheat sheet uses limits as low as 1kb for small forms. - Session middleware, then authentication, then authorisation routers.
- Routes.
- A custom 404 handler and a final error handler that logs the stack server-side and returns a generic message.
What Helmet sets by default
| Header | Helmet default | Notes for Express teams |
|---|---|---|
| Content-Security-Policy | default-src 'self', object-src 'none', script-src 'self', script-src-attr 'none', frame-ancestors 'self', upgrade-insecure-requests and others | Tune directives; start with reportOnly: true. The default style-src includes 'unsafe-inline'. |
| Strict-Transport-Security | max-age=31536000; includeSubDomains | preload is off by default; add it only after confirming every subdomain is HTTPS. |
| Cross-Origin-Opener-Policy | same-origin | Use same-origin-allow-popups if OAuth or payment pop-ups break. |
| Cross-Origin-Resource-Policy | same-origin | Relax to same-site for assets served to sibling subdomains. |
| Referrer-Policy | no-referrer | Strict; some analytics expect strict-origin-when-cross-origin. |
| X-Content-Type-Options | nosniff | Required by ASVS 5.0 V3.4.4. |
| X-Frame-Options | SAMEORIGIN | Legacy; CSP frame-ancestors is the modern control. |
| Origin-Agent-Cluster, X-DNS-Prefetch-Control, X-Download-Options, X-Permitted-Cross-Domain-Policies | ?1, off, noopen, none | Low cost; leave on. |
| X-Powered-By / X-XSS-Protection | Removed / 0 | Helmet disables the legacy XSS filter because it made things worse. |
Cross-Origin-Embedder-Policy is not set by default; enable it only if you need cross-origin isolation. The Security Headers Guide covers each header in depth.
Rate limiting choices
The Express team recommends blocking on two signals for login: consecutive failures for the same username and IP, and total failures from an IP over a longer window (their example is 100 failures in a day), and points to rate-limiter-flexible. express-rate-limit is a simpler per-route limiter with windowMs, limit and IETF RateLimit headers through standardHeaders. Choose by deployment:
- Single instance, low risk: express-rate-limit with the default memory store is acceptable.
- Several instances or containers: use a shared store (Redis or similar), or each replica counts separately and the effective limit multiplies.
- Login and recovery endpoints: key on username plus IP as well as IP alone, so distributed credential stuffing against one account is still slowed. Keep IPv6 grouping (express-rate-limit's
ipv6Subnet) so one host cannot rotate through a /64. - Large estates: enforce coarse limits at the edge or API gateway and keep account-aware limits in the app. See the Rate Limiting and API Abuse Guide.
Input, parsing and redirects
- Type checks: query strings can parse into arrays or objects. Express 5 defaults to the simple query parser and
express.urlencodedtoextended: false; keep those unless you need nested input, and validate types (ASVS 5.0 V15.3.5). Usehppor explicit handling against parameter pollution (V15.3.7). - Prototype pollution: never merge request bodies into objects with recursive merge helpers; use schema validation and
Mapwhere keys are user controlled (V15.3.6). See the Prototype Pollution Guide. - Templates: keep auto-escaping on and never compile user input as a template. See the Server-Side Template Injection (SSTI) Guide.
- Redirects: validate the target host with
new URL()against an allowlist beforeres.redirect(), and do not trustRefereras a redirect target now that Express 5 has removed the'back'shortcut. See the Open Redirect Guide. - Dangerous functions: no
eval(), nochild_process.execwith request data, and regular expressions checked for catastrophic backtracking (ReDoS), witheslint-plugin-securityin CI.
Phased rollout
- Inventory: list Express and Node.js versions, middleware, routes and which are public. Upgrade any 3.x service immediately and bring 4.x to at least 4.21.2, or plan the move to 5.
- Low-risk wins: disable X-Powered-By, add custom error handlers, set body limits and fix trust proxy. Verify
req.ipin logs from a known test client. - Headers in report mode: deploy Helmet with
contentSecurityPolicy: { reportOnly: true }, collect violations for one or two release cycles, then enforce. - Sessions: move to a production store, rename the cookie, set Secure and SameSite, and add regenerate on login. Expect a one-time logout when the cookie name changes; schedule it.
- Rate limits: start with logging-only thresholds, compare against real traffic, then enforce on authentication routes first.
- Rollback: keep each change behind configuration so a header or limiter can be relaxed without a code release, and keep the previous session secret in the array during rotation so a rollback does not invalidate sessions.
Framework mapping
| Express control | OWASP ASVS 5.0 | NIST SP 800-53 Rev 5 | ATT&CK threat addressed |
|---|---|---|---|
| Session cookie attributes, regenerate on login | V3.3.1 to V3.3.4, V7 Session Management | SC-23 Session Authenticity | T1539 Steal Web Session Cookie |
| Login rate limiting and lockout | V6 Authentication | AC-7 Unsuccessful Logon Attempts | T1110 Brute Force, T1110.004 Credential Stuffing |
| Helmet headers and CSP | V3.4.1 to V3.4.8 | SC-8 Transmission Confidentiality and Integrity (HSTS) | T1190 Exploit Public-Facing Application |
| Input validation, body limits, HPP | V15.3.5, V15.3.7, V2 | SI-10 Information Input Validation | T1190 |
| Version currency and dependency audit | V15.1.1, V15.2.1 | SI-2 Flaw Remediation, RA-5 | T1195.001 Compromise Software Dependencies and Development Tools |
| No fingerprinting or debug output | V13.4.2, V13.4.6 | CM-7 Least Functionality | Reconnaissance support |
Common misconfigurations
- trust proxy set to true on a directly reachable app: clients forge
X-Forwarded-For, bypass IP rate limits and poison audit logs. Match the setting to the real hop count or proxy subnet. - Secure cookies that silently stop working:
cookie.secure: truebehind a TLS-terminating proxy withouttrust proxymeans express-session never sets the cookie, and teams "fix" it by turning Secure off. - MemoryStore in production: sessions vanish on restart, memory grows without bound and multiple replicas disagree about who is logged in.
- Routers mounted before authentication: a new router added near the top of
app.jsis public by accident. Test unauthenticated access to every route in CI. - Helmet added, CSP disabled: teams turn off
contentSecurityPolicyto stop breakage and never return to it, losing the main XSS mitigation. Use report-only instead. See the Cross-Site Scripting (XSS) Prevention Guide. - Stack traces to users: the default error handler or a
res.send(err)leaks paths and library versions. Use a final handler that returns a generic message. - Permissive CORS on cookie-authenticated APIs: reflecting any Origin with credentials allowed lets other sites read authenticated responses. See the CORS Misconfiguration Guide.
Practitioner checklist
- Express is on a maintained major with all published security releases applied (4.21.2 or later, or 5.x), and Node.js is on a supported line.
x-powered-byis disabled and custom 404 and error handlers return no stack traces in production.trust proxymatches the real proxy path, verified by checkingreq.ipin logs.- Helmet is enabled with an application-specific CSP that was trialled in report-only mode and has a reporting endpoint.
- express-session uses a production store, a non-default cookie name, Secure, HttpOnly, SameSite and a strong rotated secret from a secrets manager.
- Sessions are regenerated on login and destroyed server-side on logout.
- Login, reset and token endpoints have account-aware rate limits backed by a shared store.
- Body parsers have explicit size limits, input is type-checked and schema-validated, and redirects use an allowlist.
- Authentication is applied at router level with a documented public route list tested in CI.
npm ci,npm auditand a lint step with security rules run on every build.
Standards and references
- Express: Production Best Practices: Security
- Express: Security Updates
- Express: Express Behind Proxies
- Express: Migrating to Express 5
- Express: express-session documentation
- Helmet: Helmet documentation
- OWASP: Node.js Security Cheat Sheet
- OWASP: Application Security Verification Standard 5.0
Related NHI Mgmt Group resources: Node.js Security Guide · Security Headers Guide · Cookie Security Guide · Rate Limiting and API Abuse Guide · Server-Side Template Injection (SSTI) Guide