Security
Deserialization at the framework boundary
One RCE at CVSS 10.0, then six denial-of-service patches in eight months. When the contract is "accept whatever the format encodes", fixing one lever leaves the rest.
Between December 2025 and June 2026, @nestjs/platform-fastify shipped four separate security releases for what is, read closely, the same vulnerability:
CVE-2025-69211 — middleware skipped on a percent-encoded path. Fixed in 11.1.11.CVE-2026-2293 — skipped again when Fastify's path-normalization options are enabled. Fixed in 11.1.14.CVE-2026-33011 — skipped by sending HEAD instead of GET, because Fastify routes a HEAD request to the matching GET handler. Fixed in 11.1.16.CVE-2026-54281 — skipped by appending a trailing slash to the URL. Fixed in 11.1.24.Four fixes in seven months. Each one is correct and each one was worth shipping. But a bug that keeps returning under a new number is usually not a bug. It is a design position that does not hold.
Route-scoped middleware means two systems read the same request and each decides what it is. The middleware layer matches the path against a pattern to decide whether to run. The router matches the same path against a route to decide what to execute. Those are two separate implementations of the question "which route is this?", and every advisory above is a case where they answered it differently.
/admin and /admin/ are one route to the router and two strings to the matcher. A percent-encoded path is one thing before decoding and another after. HEAD /admin matches no GET middleware and reaches the GET handler anyway. The handler runs. The guard did not.
This class cannot be closed by enumerating cases. The list is as long as the set of strings two parsers can disagree about, and it grows every time either parser gains an option.
CVE-2025-29927, CVSS 9.1: a request carrying an x-middleware-subrequest header was treated as an internal call and skipped middleware entirely. Patched in 15.2.3, 14.2.25, 13.5.9 and 12.3.5. Not a parser disagreement this time — a trust boundary that accepted a client-supplied header as proof of internal origin — but the same outcome for the same structural reason: the authorization decision sat somewhere a request could get past.
Then it happened again. CVE-2026-64642, July 2026: App Router applications built with Turbopack and exactly one entry in config.i18n.locales could have middleware-based authentication bypassed. Fixed in 16.2.11. The advisory's own workaround is worth reading as written:
enforce authorization in the page's server-side data path
instead of relying solely on middlewareThat is the framework telling you where the check belongs.
middleware.ts that redirects unauthenticated users, and pages or route handlers that assume it ran?MiddlewareConsumer.forRoutes() call the only authorization check standing in front of a route?If the answer to the third question is yes, your middleware is your access control, and it is one parser disagreement away from being absent.
At the point the data is read. The function that fetches the record is the last place in the request that cannot be routed around, because there is no second implementation of it competing to decide what it matched. There is one function that reads that row, and either the check inside it runs or the row is not read.
In practice: the session lookup and the ownership test live in the data layer, or in one function every handler calls before it returns anything. That is more code than a single middleware file, and it is repeated in a way that looks redundant right up until the day it is not. It is also the only version that survives the framework changing its mind about what a trailing slash means.
Everything that is not a security decision. Locale negotiation, redirects, adding response headers, rejecting obviously malformed traffic early so the expensive path never runs. It is a good place to make a cheap decision and a bad place to make the only one.
Treat it as an optimisation. If it is skipped, the request should get slower — not more privileged.
Security
One RCE at CVSS 10.0, then six denial-of-service patches in eight months. When the contract is "accept whatever the format encodes", fixing one lever leaves the rest.
Security
Eighteen malicious versions of one package went out in two and a half hours, harvesting tokens on install. Lockfiles, disabled scripts and short-lived credentials.
Security
Sixteen advisories in one day across fourteen projects, and most of them say access bypass. The fix is not more diligence, it is a shorter module list.