# Risk map — expressjs/express

> Canonical page: https://codevetter.com/unpack/express/risk-map

# Risk map — expressjs/express

Scanned commit 9a34acf03cb818ff3f8bc40e44176e277a25cbb9 (2026-09-15). Generated by codevetter codevetter 1.12.0 on 2026-09-22.

The security-sensitive surface concentrates in four places: proxy trust (which decides whether client-supplied `X-Forwarded-*` headers are believed), JSONP callback sanitisation, file transfer via `res.sendFile`/`res.download`, and dynamic `require()` inside the view system. Blast radius is dominated by `res.send` — `json`, `jsonp`, `sendStatus` and the default `render` callback all funnel through it, so any change there touches nearly every response. Coupling risks come from prototype mutation: `app.handle` re-prototypes live Node objects and mounting re-parents settings chains, so behaviour can change at a distance. Supply-chain posture is strong, but `.npmrc` disables lockfiles, so local installs are not reproducible.
- SECURITY — proxy trust: `req.ip`, `req.ips`, `req.protocol`, `req.secure` and `req.host` all believe client-supplied headers whenever `trust proxy fn` returns true; setting `trust proxy` to `true` makes `compileTrust` return a constant-true function, so `X-Forwarded-For`/`-Proto`/`-Host` become fully spoofable. — [lib/utils.js#L194-L215](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/utils.js#L194-L215) [lib/request.js#L297-L343](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/request.js#L297-L343) [lib/request.js#L418-L431](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/request.js#L418-L431)
- SECURITY — dynamic require: the `View` constructor derives a module name from the view's file extension and calls `require(mod)`. An application that renders a view name built from user input can drive module resolution, and the thrown errors leak which module was attempted. — [lib/view.js#L58-L88](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/view.js#L58-L88)
- SECURITY — JSONP: the callback is taken from `req.query` and filtered with `replace(/[^\[\]\w$.]/g, '')`; correctness of this allowlist plus the `/**/ typeof` wrapper and forced `X-Content-Type-Options: nosniff` are the only things standing between `res.jsonp` and reflected script injection. — [lib/response.js#L264-L318](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/response.js#L264-L318)
- SECURITY — prototype keys: the `extended` query parser calls `qs.parse` with `allowPrototypes: true`, which is a deliberate compatibility choice that permits keys like `constructor` in parsed query objects. — [lib/utils.js#L267-L271](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/utils.js#L267-L271)
- SECURITY — file IO outside repo root: `res.sendFile` only enforces "absolute path or `root` option" and `encodeURI`; every actual containment check is delegated to the `send` package, so a `send` regression is an Express path-traversal regression. — [lib/response.js#L398-L420](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/response.js#L398-L420) [package.json#L52](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/package.json#L52)
- SECURITY — information disclosure by default: `x-powered-by` is enabled in `defaultConfiguration` and `app.handle` sets the `X-Powered-By: Express` header on every response unless explicitly disabled. — [lib/application.js#L94-L95](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/application.js#L94-L95) [lib/application.js#L160-L163](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/application.js#L160-L163)
- SECURITY — cookie signing depends on a foreign property: `res.cookie` reads `this.req.secret`, which Express never sets; it throws `cookieParser("secret") required for signed cookies` if `cookie-parser` was not installed upstream, making signing silently middleware-order dependent. — [lib/response.js#L747-L755](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/response.js#L747-L755)
- BLAST RADIUS — `res.send` is the shared exit for `res.json`, `res.jsonp`, `res.sendStatus` and the default `res.render` callback; the HEAD-vs-body, 204/304/205 header stripping, ETag and Content-Length rules all live in that one function. — [lib/response.js#L126-L226](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/response.js#L126-L226) [lib/response.js#L236-L334](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/response.js#L236-L334) [lib/response.js#L896-L920](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/response.js#L896-L920)
- FRAGILE COUPLING — mutation of live Node objects: `app.handle` calls `Object.setPrototypeOf` on the actual `IncomingMessage`/`ServerResponse`, and `mounted_app` must manually restore the parent prototypes after a sub-app completes; forgetting that restore silently leaks the child's prototype into sibling middleware. — [lib/application.js#L168-L182](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/application.js#L168-L182) [lib/application.js#L217-L226](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/application.js#L217-L226)
- FRAGILE COUPLING — settings inheritance is by prototype chain (`Object.setPrototypeOf(this.settings, parent.settings)`), guarded by a string symbol `'@@symbol:trust_proxy_default'` that is deleted/redefined in two separate places to preserve back-compat. — [lib/application.js#L47](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/application.js#L47) [lib/application.js#L100-L123](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/application.js#L100-L123) [lib/application.js#L371-L380](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/application.js#L371-L380)
- PERFORMANCE — synchronous file IO on the request path: `View#resolve` calls `fs.statSync` up to twice per lookup, and `view cache` is only auto-enabled when `NODE_ENV === 'production'`, so non-production deployments stat the filesystem on every render. — [lib/view.js#L169-L204](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/view.js#L169-L204) [lib/application.js#L143-L145](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/application.js#L143-L145)
- PERFORMANCE — `req.query`, `req.host`, `req.protocol` and `req.ip` are non-memoised getters; each access re-parses. Middleware that reads `req.query` repeatedly pays the `qs`/`querystring` cost every time. (inferred) — [lib/request.js#L230-L241](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/request.js#L230-L241) [lib/request.js#L521-L527](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/request.js#L521-L527)
- LEGACY — `depd` deprecation paths remain live in `res.redirect` for missing URLs, non-string URLs and non-number statuses, meaning three invalid-argument shapes still produce a response instead of throwing. — [lib/response.js#L814-L840](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/response.js#L814-L840) [lib/response.js#L17](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/response.js#L17)
- LEGACY — `legacy.yml` still runs the full suite on Node 16 and 17 even though `package.json` declares `engines.node >= 18`; the two signals disagree about the supported floor. — [.github/workflows/legacy.yml#L30-L36](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/.github/workflows/legacy.yml#L30-L36) [package.json#L91-L93](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/package.json#L91-L93)
- REPRODUCIBILITY — `.npmrc` sets `package-lock=false`, and no lockfile is committed, so CI and local installs resolve dependency versions independently at install time; `min-release-age=7` is the mitigation. — [.npmrc](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/.npmrc) [.gitignore](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/.gitignore)
- SUBTLE API — `res.download` rebinds options with `opts = Object.create(opts)` so caller options live on the prototype rather than as own properties; any downstream code doing `Object.keys(opts)` or a spread will not see them. — [lib/response.js#L475-L480](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/response.js#L475-L480)
- BREAKING-CHANGE HAZARD — `res.status` now throws `TypeError` for non-integers and `RangeError` outside 100–999, so any code path that computed a status dynamically can convert a soft bug into a thrown exception mid-response. — [lib/response.js#L65-L78](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/response.js#L65-L78) [test/res.status.js](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/test/res.status.js)

Every claim links to its cited files at the pinned commit. Citations identify source evidence; they do not establish runtime correctness. Independent CodeVetter scan — not reviewed by upstream maintainers.

## Public product links

- [CodeVetter](https://codevetter.com/)
- [Download](https://codevetter.com/download)
- [Documentation](https://codevetter.com/docs/)
- [Source](https://github.com/Codevetter/codevetter)
