Fix cards for Node.js and Express

A01:2025 Broken Access Control

  • Cross-site request forgery (CSRF)CWE-352

    Express has no built-in CSRF protection, so add one middleware before every route: let POST, PUT, PATCH and DELETE through only when Sec-Fetch-Site is same-origin (or none), fall back to an exact Origin match when the browser sends no Sec-Fetch-Site, and refuse the rest with 403.

  • Missing object-level authorization (IDOR and BOLA)CWE-862 · CWE-639 · CWE-863

    Take the user from your authentication middleware (req.user), never from the request, and put the same scope in every invoice query: tenant_id = $tenant AND (owner_id = $user OR $isBillingAdmin).

  • Open redirectCWE-601

    Resolve next against your own origin with new URL(next, site), the WHATWG parser browsers use, and redirect only if url.origin is in an exact allowlist and the URL carries no user name or password; otherwise redirect to a fixed page.

  • Path traversalCWE-22 · CWE-23 · CWE-36 · CWE-59

    Check where the path lands, not what it looks like: resolve base with fs.realpath, resolve the request with realpath(path.resolve(base, name)), which collapses ../ and follows symlinks, and serve it only if it starts with base + path.sep.

  • Server-side request forgery (SSRF)CWE-918

    Attach the policy to the connection, not the hostname string.

A02:2025 Security Misconfiguration

A04:2025 Cryptographic Failures

  • Insecure randomness for security tokensCWE-330 · CWE-338

    Generate the token with a CSPRNG: crypto.randomBytes(32).toString('base64url') is 256 bits (prefer it over crypto.randomUUID, which carries only 122).

  • Weak password hashingCWE-916 · CWE-759 · CWE-327

    Hash with Argon2id at or above OWASP's minimum: argon2.hash(password, { type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1 }).

A05:2025 Injection

  • Code injectionCWE-94 · CWE-95

    Do not turn a request string into code. eval, new Function and node:vm all run it, and node:vm is not a sandbox.

  • Cross-site scripting (XSS)CWE-79

    Print every field through EJS's escaping tag <%= %>, which turns & < > " and ' into entities, and never through the raw tag <%- %>.

  • CSV formula injectionCWE-1236

    Spreadsheets run a cell that starts with =, +, -, @, tab or carriage return.

  • NoSQL operator injectionCWE-943

    Never let a request value reach a MongoDB filter as an object: accept strings only (typeof value === 'string'), compare with { field: { $eq: value } }, and check secrets such as reset tokens in code with timingSafeEqual after looking the account up by email.

  • OS command and argument injectionCWE-78 · CWE-88

    Run the program directly with its arguments as a list - execFileSync(tool, ['--', name]) - so no shell parses them, and put '--' before any user operand so a leading dash cannot pose as an option.

  • Server-side template injectionCWE-1336 · CWE-94 · CWE-95

    Don't compile input as a template: ejs.render(userTemplate) runs any <%= ... %> as JavaScript.

  • SQL injectionCWE-89

    Bind every value: put ? placeholders in the SQL and pass the values to node:sqlite, as in db.prepare(sql).all(name, status).

A06:2025 Insecure Design

  • ClickjackingCWE-1021

    Send a framing policy on sensitive pages. Content-Security-Policy: frame-ancestors 'none' (or 'self' and named partners) is what modern browsers honor; add X-Frame-Options: DENY for old ones.

  • Regular-expression denial of service (ReDoS)CWE-1333

    A validation regex with a nested quantifier backtracks catastrophically: a 40-character string pins a CPU for minutes and, on Node's single event loop, blocks every request.

  • Unbounded resource consumptionCWE-770 · CWE-400

    Bound every resource a request can consume: cap the body size as it is read (express.json's limit), cap the work one request asks for (a maximum item count), and rate-limit callers keyed on the connection address, not a header the client sets.

  • Unrestricted file uploadCWE-434 · CWE-646 · CWE-73 · CWE-770

    Decide the type from the bytes, and never let the client pick the name.

A07:2025 Authentication Failures

  • Hard-coded secretsCWE-798 · CWE-260

    Read the key from process.env at startup, where the deployment puts it from your secret store, and throw if it is missing or blank so the app never starts without it.

  • Improper TLS certificate validationCWE-295 · CWE-297 · CWE-296

    Never turn verification off. Give the partner client an https.Agent whose ca is the partner's private CA bundle, set rejectUnauthorized: true explicitly so NODE_TLS_REJECT_UNAUTHORIZED=0 cannot flip it, and keep the default checkServerIdentity.

  • JWT validation in APIs (bearer tokens)CWE-347 · CWE-287

    Verify every bearer token with jose's jwtVerify(): pass the issuer's public key from configuration, algorithms: ['RS256'], issuer and audience, requiredClaims: ['exp', 'sub'] and a small clockTolerance.

  • Missing authentication for a critical functionCWE-306 · CWE-287

    Verify a credential on the server before any critical route runs.

  • Session fixation and session cookiesCWE-384 · CWE-613 · CWE-614 · CWE-1004 · CWE-1275

    In the sign-in handler, call req.session.regenerate(), then store the user and save, so no session ID from before sign-in survives.

A08:2025 Software or Data Integrity Failures

  • Insecure deserializationCWE-502

    Treat the token as data: JSON.parse it and rebuild only an allowlisted type - map $type to one known class, never the class the payload names.

  • Mass assignment (over-posting)CWE-915

    Never pass req.body to Object.assign, a spread or an ORM create or update on a stored record.

A09:2025 Security Logging and Alerting Failures

Not covered in Node.js yet

These weaknesses have fix cards in other stacks but not in this one.

  • XML external entity (XXE) processing: No Node.js XML parser that resolves attacker-controlled entities installs cleanly on Node 26, so a vulnerable-vs-fixed pair cannot be built honestly on this runtime. Verified: libxmljs2 0.37.0 (the libxml2 binding) fails to compile against Node 26.3.0's V8 ABI (node-gyp build error, GetAlignedPointerFromInternalField signature change); fast-xml-parser 5.3.6 does not expand DOCTYPE-declared entities (it keeps '&c;' literal) and throws 'External entities are not supported' (only programmatic addEntity expands), and its entity-expansion flaw CVE-2026-26278 affects only the older 4.1.3-5.3.5 range, which the pinning rule forbids shipping; @xmldom/xmldom 0.9.8 reports 'entity not found' for custom and external entities and resolves neither. That pure-JS XML parsers do not dereference external entities is itself a useful defensive fact: on Node the exposure is overwhelmingly entity-expansion (CWE-776) in older or misconfigured libraries, addressed by keeping the parser current and disabling DOCTYPE processing.
  • LDAP filter injection
  • XPath injection

Supported versions

Only the latest release of each supported major line gets fixes; the Express team lists 5.x and 4.x as supported with no end date. Express support policy.

Express
VersionLatestStatusReleasedEnd of support
5.x5.3.0SupportedCurrent major; the cards use it.10 September 2024No date set
4.x4.22.3Supported9 April 2014No date set
Node.js
VersionLatestStatusReleasedEnd of support
2626.11.1SupportedCurrent release until 2026-10-27; Active LTS from 2026-10-28 and Maintenance LTS from 2027-10-20 on the release schedule.5 May 202630 April 2029
2424.21.0SupportedActive LTS until 2026-10-19; Maintenance LTS from 2026-10-20 on the release schedule.6 May 202530 April 2028
2222.23.3MaintenanceMaintenance LTS since 2025-10-21.24 April 202430 April 2027
2020.20.2End of life18 April 202330 April 2026

Production applications should run an Active or Maintenance LTS line. The Node.js release schedule moves Node.js 24 from Active to Maintenance LTS on 20 October 2026 and Node.js 26 from Current to Active LTS on 28 October 2026. Node.js release schedule

How the fix cards choose versions

Minimum secure versions

The lowest release in each supported line that fixes every published advisory listed here. Anything older is missing a security fix.

PackageAt leastWhy
express5.3.0Requires proxy-addr 2.0.8 (CVE-2026-90711, IP spoofing), qs 6.16.0 (CVE-2026-82417, CVE-2026-82562 and CVE-2026-2391, denial of service) and body-parser 2.3.0 (CVE-2026-12590); npm ls qs should show only 6.16.0. Advisory
express4.22.3Requires qs 6.16.0 and path-to-regexp 0.1.13 (CVE-2026-4867). Refresh the lockfile until proxy-addr is 2.0.8 (CVE-2026-90711) and body-parser 1.20.8: 1.20.6 still pulls a vulnerable qs 6.15.3. Advisory
node22.23.2, 24.18.1 or 26.5.1The 29 July 2026 security releases, the latest on 11 October 2026: an HTTP/2 use-after-free (CVE-2026-56848), a Permission Model bypass (CVE-2026-58043), HTTP/2 memory exhaustion in 22.x and 24.x (CVE-2026-56846) and eight lower-severity CVEs. Advisory

Middleware and security order

  1. Trust proxy: Set app.set('trust proxy', 1) to the number of proxies you run before anything reads req.ip or req.protocol; express-session only sends a secure cookie when Express knows the request arrived over HTTPS.
  2. Security headers: Mount helmet() first, so every response, including 404s and errors, carries the headers; it also removes X-Powered-By.
  3. Body parsers with limits: express.json() and express.urlencoded({ extended: false }) with an explicit limit (the default is 100kb), only on routes that take a body.
  4. Session: express-session with a production store (the default MemoryStore is not designed for production), secure, httpOnly, sameSite and a generic cookie name.
  5. CSRF protection and rate limits: After the session and body parsers and before the routes that change state; rate-limit sign-in and other credential endpoints.
  6. Routes: Authenticate in router-level middleware and check object ownership inside each handler or its data-access call.
  7. 404 handler, then the error handler: A four-argument error handler last, returning a generic message; run with NODE_ENV=production so the built-in handler never sends stack traces.

Secure defaults and settings

SettingValueWhy
query parser'simple' (Express 5)Keep it. The 'extended' (qs) parser turns a[b]=c into nested objects, which is how operator objects reach NoSQL queries.
x-powered-byEnabledDisable it with app.disable('x-powered-by') or Helmet.
express-session cookiehttpOnly: true, secure: false, no sameSiteSet secure: true and sameSite: 'lax', and regenerate the session at sign-in.
express.json() limit'100kb'Set the smallest limit each route needs.
NODE_ENVUnset (development)Set production; otherwise the built-in error handler sends stack traces to the client.

Escape hatches to search for

Framework APIs that switch a protection off. Each one is sometimes needed; search for them in review and make each use justify itself.

APIRiskInstead
EJS <%- value %>Writes the value without HTML escaping.<%= value %>, or a sanitizer for HTML you must render.
res.send() with HTML built in a template stringNo escaping at all.A template engine that escapes by default.
cors({ origin: true, credentials: true })Reflects any Origin and allows credentials.An explicit list of origins.
app.set('trust proxy', true)Trusts any X-Forwarded-For, so clients choose their own req.ip.The hop count or the proxy's address range.
app.set('query parser', 'extended')Query values become objects and arrays the code does not expect.The simple parser and type checks on every value.
express.static(root, { dotfiles: 'allow' })Serves .env and .git files.The default 'ignore', and a static root that holds only public files.

Checklist, mapped to ASVS 5.0.0

  • v5.0.0-3.4.1 Every response carries Strict-Transport-Security with a max-age of at least a year (Helmet's default is 365 days with includeSubDomains).
  • v5.0.0-3.3.1 Session cookies are Secure and use the __Host- or __Secure- prefix.
  • v5.0.0-7.2.4 Sign-in calls req.session.regenerate() so the user gets a new session ID.
  • v5.0.0-3.5.1 State-changing requests from browsers carry a CSRF token or pass an Origin check.
  • v5.0.0-1.2.4 Every query binds its values; no SQL is built with template literals.
  • v5.0.0-8.2.2 Each handler checks that the caller owns the object it reads or changes.
  • v5.0.0-15.3.3 Request bodies are parsed into a schema that lists the fields a caller may set.
  • v5.0.0-4.1.3 trust proxy matches your proxies, so clients cannot set their own address or scheme.
  • v5.0.0-16.5.1 Errors reach the client as a generic message; details go to the server log.

References

  1. Express: production best practices, security
  2. Express: version support
  3. Express: error handling
  4. express-session README
  5. Helmet
  6. Node.js release schedule (schedule.json)
  7. Node.js July 2026 security releases

All fix cards