If you are using NestJS with Fastify, your middleware might be blind. An attacker can access protected routes like `/admin` by requesting `/%61dmin`. The middleware sees a mismatch and ignores it, but the underlying Fastify router decodes it and serves the restricted content. Patch immediately to version 11.1.11.
A critical normalization discrepancy in the NestJS Fastify adapter allows attackers to bypass middleware security checks simply by URL-encoding characters in the request path.
Imagine a nightclub bouncer who has a very specific list of banned guests. He sees "Smith" on the list and stops him. But if "Smith" shows up wearing a fake mustache and calls himself "Sm%69th", the bouncer shrugs and lets him in. Inside the club, the bartender (the application logic) recognizes him immediately as Smith and serves him the drinks he's not supposed to have.
This is essentially what happened in CVE-2025-69211. NestJS, a darling of the Node.js enterprise world, supports multiple HTTP adapters. If you were using the default Express adapter, you were fine. But if you opted for the high-performance Fastify adapter (@nestjs/platform-fastify), you unknowingly introduced a subtle, dangerous gap between your security middleware and your actual application logic.
The vulnerability is a classic "Parser Differential" or Logical Time-of-Check Time-of-Use (TOCTOU). The component checking your credentials sees one thing, but the component serving the data sees another. It's simple, elegant, and devastatingly effective against middleware-based authentication.
The root cause lies in how NestJS handles middleware matching versus how Fastify handles routing. When you define middleware in NestJS, you often restrict it to specific routes using .forRoutes('admin'). You expect this to protect anything under /admin.
Here is where the logic splits:
This created a blind spot. An attacker sends GET /%61dmin. The middleware regex looks at /%61dmin, compares it to /admin, says "Nope, no match here," and steps aside. The request flows through to Fastify. Fastify sees /%61dmin, decodes %61 to a, resolves it to /admin, and executes the sensitive controller code.
The fix is technically simple but conceptually vital: Normalize before you validate. The developers had to ensure that the middleware engine sees the same URL that the router will eventually see.
In the commit c4cedda15a05aafec1e6045b36b0335ab850e771, the file packages/platform-fastify/adapters/middie/fastify-middie.ts was updated. The patch introduces explicit decoding using safeDecodeURI before running the path matching logic.
Before (Vulnerable):
// The code implicitly trusted the raw URL
if (regexp.exec(url)) {
// execute middleware
}After (Patched):
import { safeDecodeURI } from 'find-my-way'; // Fastify's router lib
// ... inside the handler
if (regexp) {
// Decode URL before matching to prevent bypass
const decodedUrl = safeDecodeURI(url).path;
const result = regexp.exec(decodedUrl);
if (result) {
// execute middleware
}
}By forcing the URL to be decoded before the regex check, the middleware now correctly identifies /%61dmin as /admin, triggering the security checks.
Let's assume a standard NestJS setup where an AuthMiddleware checks for a valid JWT on all routes starting with /users. We want to steal user data, but we don't have a token.
Step 1: Verify the Target
We send a normal request:
GET /users/101
Response: 401 Unauthorized (The middleware caught us).
Step 2: The Encode Trick
We take the first character of the path, u, and convert it to its hex equivalent %75.
Request: GET /%75sers/101
Step 3: The Bypass
/%75sers/101 against /users. No match. Execution proceeds without checking for a token./%75sers/101 to /users/101. Finds the UsersController.200 OK. We have the data.This technique works for any character in the path that doesn't change the semantic meaning when decoded. It effectively renders path-based middleware useless for security boundaries.
The immediate fix is to update the @nestjs/platform-fastify package to version 11.1.11 or later. This includes the logic to decode URLs prior to middleware matching.
However, this vulnerability highlights a broader architectural lesson in NestJS security: Do not rely on Middleware for Authorization.
Why? Middleware in Express/NestJS is a low-level wrapper around the HTTP request. It often runs before the routing context is fully established. In contrast, NestJS Guards run after the router has identified which handler will process the request, but before the handler executes. Guards are context-aware and are bound to the controller method, not a string path regex.
[!TIP] Pro Tip: Migrate your security logic (Authentication/Authorization) to Guards. Guards are not susceptible to this specific URL encoding bypass because they don't rely on string matching the URL path; they rely on the execution context of the class/method being invoked.
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
@nestjs/platform-fastify NestJS | < 11.1.11 | 11.1.11 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-367 (Time-of-Check Time-of-Use) |
| CVSS Score | 6.9 (Medium) |
| Attack Vector | Network (URL Encoding) |
| Impact | Security Bypass / Authorization Bypass |
| Affected Component | @nestjs/platform-fastify |
| Fix Version | 11.1.11 |
The product checks the state of a resource before using it, but the resource's state can change between the check and the use in a way that invalidates the results of the check.
Get the latest CVE analysis reports delivered to your inbox.