Best Practices

Express.js Security Middleware: An Audit

Express remains the default Node.js framework at most shops, and its middleware ecosystem is a thirteen-year accumulation of packages, some abandoned, some indispensable. This is a pragmatic audit of what belongs in a 2023 Express stack.

Nayan Dey
Senior Security Engineer
6 min read

Express turned thirteen years old in 2023, and the ecosystem that grew around it shows every year. I spent a quarter auditing a portfolio of eleven Express applications for a financial services client, and I kept notebooks of what I found in each middleware chain. This is the consolidated version — the patterns, the abandoned packages, and the CVEs that keep reappearing because nobody audits their app.use stack.

Express itself is mostly fine. The 4.x line has been stable for years, and the 4.21.0 release in September 2024 patched the open redirect CVE-2024-43796 quickly enough that most teams upgraded within a month. The problem is almost never Express. The problem is the fifteen middleware packages sitting on top of it, three of which were last published in 2019 and one of which has a maintainer who has publicly said he is no longer interested.

Body parsers: stop using the deprecated ones

I still find codebases using body-parser as a direct dependency, imported separately from Express. As of Express 4.16 (released in 2017, for context), the parsers are built in: express.json() and express.urlencoded(). There is no reason to have body-parser in your package.json unless you are maintaining a library that targets older Express. Remove it; one fewer dependency is one fewer thing to audit.

The real body-parser risk is payload size. The default limit is 100kb, which is fine for most APIs, but I have watched teams bump it to '50mb' because one upload endpoint needed it, and then leave that limit applied globally. That is a DoS vector — a few hundred concurrent 50MB POSTs to any route will pin your event loop. Apply large payload limits at the route level, not the app level.

The helmet situation

Helmet 7.0 shipped in April 2023 and became the stable baseline for CSP, HSTS, X-Frame-Options, and the rest of the response header hardening suite. If your project still pins helmet@4.x, you are missing three years of default improvements, including the removal of the dangerously permissive default contentSecurityPolicy directives that allowed unsafe-inline. Upgrade, review the defaults, and add explicit CSP directives for your actual hosts.

One pattern I flag in every audit: using helmet without configuring CSP reporting. Ship a report-uri or report-to directive to an endpoint you actually monitor. Otherwise, you will not know when a legitimate integration breaks versus when an attacker is probing for XSS sinks.

Rate limiting: express-rate-limit is the right answer, but not by default

The Express ecosystem has several rate limiters. express-rate-limit is the one I see most often, and it is fine, but the default in-memory store does not share state across process instances. In any production deployment running under PM2, Kubernetes, or a load balancer, you need the Redis store. I have audited apps that thought they were rate-limiting at 100 requests per minute and were actually allowing 1,600 because they ran sixteen pods, each with its own counter.

The other gotcha is trusting req.ip. If you are behind a proxy — and you almost always are — you need app.set('trust proxy', ...) configured correctly, or every rate limit bucket is keyed on the load balancer IP. Test this explicitly; do not assume.

The csurf ghost

csurf was deprecated in September 2022. The maintainers explicitly archived the repository and pointed users to alternatives. Two years later, I still find it in roughly a third of the Express codebases I audit. It carries a prototype pollution advisory (CVE-2022-24434 via a dependency) that will never be fixed because nobody is fixing csurf anymore.

If you need CSRF protection for a cookie-authenticated web app, the modern options are csrf-csrf (a maintained fork with the double-submit cookie pattern), lusca, or rolling your own with the SameSite=Strict cookie attribute plus an origin check. If you are building an API consumed by a SPA using bearer tokens, you often do not need CSRF protection at all — but verify your authentication is actually bearer-token based and not relying on cookies.

Passport and the auth middleware graveyard

Passport is the incumbent authentication middleware for Express. It has over 500 strategy plugins, many of which have not been updated in five or more years. I routinely find apps pulling passport-facebook@2.x or passport-linkedin with last-publish dates in 2017. These strategies often bundle HTTP clients, JWT libraries, and crypto code that predate modern best practices.

Audit every Passport strategy in your app. For each one, check the last publish date, the number of open issues, and whether the underlying OAuth provider has changed its protocol since the strategy was last updated. For major providers (Google, GitHub, Microsoft), migrate to their official SDKs or to openid-client, which is maintained by the panva team and supports modern OIDC.

The session store question

express-session ships with a memory store by default, and the warning in the README is the most ignored warning in Node.js. I have found production deployments using the memory store, which means every time a pod restarted, every user was logged out. More importantly, every time a pod was scaled, sessions were inconsistent.

Pick a store: connect-redis, connect-pg-simple, or connect-mongo are the three I see most. Each has its own security properties. connect-redis needs the Redis connection to be over TLS if it crosses a network boundary, which I have seen misconfigured more times than I can count.

File uploads: multer has sharp edges

Multer is the canonical Express file upload middleware. It is maintained, it works, and it has a history of CVEs worth knowing about. CVE-2022-24434 (yes, another one with that number across packages) hit dicer, a multer dependency, with a DoS via malformed multipart payloads. Multer 1.4.5-lts.1 patched it. Make sure you are on that or later.

The operational issues matter more than the CVEs. Multer writes uploads to disk by default — to /tmp or wherever os.tmpdir() points. If your app runs in a container with a small writable layer, you will exhaust disk and crash. Use multer.memoryStorage() with strict size limits for small uploads, or stream directly to object storage with multer-s3 for large ones, but do not leave the defaults in production.

The packages I tell teams to remove

Every audit, I hand over a list of packages to remove. The recurring offenders: express-validator in favor of Zod or Joi at the route level; morgan in favor of structured logging via Pino; compression only when not already handled at the reverse proxy; and cors configured with a reflective origin (which is almost always wrong). Each removal is one fewer dependency to track for CVEs.

How Safeguard Helps

Safeguard scans the Express dependency tree with reachability analysis, so you know which of those Passport strategies and body parsers are actually loaded at runtime versus sitting dormant in node_modules. Griffin AI reviews your middleware chain against known CVE patterns and flags deprecated packages like csurf, along with suggested maintained replacements. The SBOM export gives you a living inventory of every middleware and its last-publish date, which is what auditors actually want. Policy gates block pull requests that introduce abandoned packages or that downgrade helmet to a vulnerable version, catching regressions before they hit main.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.