Skip to content

CVE-2026-14198: Verified Reproduction

CVE-2026-14198: @fastify/middie encoded slash bypass on parameterized middleware paths

CVE-2026-14198 is verified against the affected target. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00207.

REPRO-2026-00207 Jul 2, 2026 CVE entry .txt
Severity
CRITICAL
CVSS
9.1
Confidence
HIGH
Reproduced in
9m 46s
Tool calls
128
Spend
$1.79
01 · Overview

What Is CVE-2026-14198?

CVE-2026-14198 is a critical authentication/authorization bypass (CWE-436) in @fastify/middie, caused by the middleware layer and Fastify's router disagreeing about a request's canonical path. Pruva reproduced it (reproduction REPRO-2026-00207).

02 · Severity & CVSS

CVE-2026-14198 Severity & CVSS Score

CVE-2026-14198 is rated critical severity, with a CVSS base score of 9.1 out of 10.

CRITICAL threat level
9.1 / 10 CVSS base
Weakness CWE-436

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

How to Reproduce CVE-2026-14198

$ pruva-verify REPRO-2026-00207
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00207/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-14198

Authorization bypass — reproduced
  • reached the target end-to-end
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

URL path with encoded slash %2F in the parameter position, e.g. GET /user/a%2Fb/comments sent without the x-api-key header

Attack chain
  1. Middie normalizePathForMatching (lib/engine.js) decodes %2F to '/' via FindMyWay.sanitizeUrlPath before matching the parameterized guard /user/:id/comments, so the guard regexp fails to match while Fastify's router (find-my-way) preserves %2F and still dispatches the /user/:id/comments handler; the unauthenticated request reaches the protected handler (HTTP 200).
How the agent worked 280 events · 128 tool calls · 10 min
10 minDuration
128Tool calls
67Reasoning steps
280Events
Agent activity over 10 min
Support
16
Hypothesis
2
Repro
81
Judge
27
Variant
150
0:0009:46

Root Cause and Exploit Chain for CVE-2026-14198

Versions: 9.1.0 – 9.3.2 (confirmed against 9.3.2; fixed in 9.3.3).

@fastify/middie versions 9.1.0 through 9.3.2 decode the percent-encoded slash %2F inside path-parameter values before matching middleware paths, while Fastify's underlying router (find-my-way) preserves the encoding during route lookup. The two layers therefore disagree on the canonical request path: middie normalizes /user/a%2Fb/comments to /user/a/b/comments (which no longer matches the guard /user/:id/comments), but the router still matches the route and dispatches to the handler. The result is an HTTP-method-agnostic authentication / authorization bypass: an unauthenticated attacker reaches a protected handler on a parameterized path by embedding an encoded slash in the parameter position.

  • Package / component affected: @fastify/middie (lib/engine.js, normalizePathForMatching).
  • Affected versions: 9.1.0 – 9.3.2 (confirmed against 9.3.2; fixed in 9.3.3).
  • Risk level: Critical. Any application that uses middie middleware for authentication, authorization, rate limiting, or auditing on a parameterized path (e.g. /api/:resource, /user/:id/comments) can have that guard silently bypassed with a single crafted URL. No authentication or preconditions are required and the bypass is HTTP-method agnostic.

Impact Parity

  • Disclosed / claimed maximum impact: Authentication/authorization bypass on parameterized middleware paths; an attacker reaches a protected handler without credentials.
  • Reproduced impact from this run: A real Fastify + middie server with an API-key auth guard on /user/:id/comments returns 200 {"ok":true,"id":"a/b"} for an unauthenticated request to /user/a%2Fb/comments — the guard is bypassed and the protected handler executes. The same request against the fixed build returns 401 Unauthorized.
  • Parity: full. The exact claimed bypass (unauthenticated reach of a protected parameterized-path handler via an encoded slash) was demonstrated through both the Fastify app.inject library entrypoint and a real 127.0.0.1 HTTP server.
  • Not demonstrated: This is an authorization/authn bypass, not memory corruption or code execution; no crash or RCE is claimed or reproduced.

Root Cause

In lib/engine.js, every request is normalized for middleware matching via normalizePathForMatching(url, options). In the vulnerable version that function calls:

path = FindMyWay.sanitizeUrlPath(path, options.useSemicolonDelimiter)

sanitizeUrlPath decodes percent-encoded characters, so %2F becomes a literal /. When a guard is registered on a parameterized prefix such as /user/:id/comments (compiled with path-to-regexp, end:false), the decoded path /user/a/b/comments has an extra segment and fails to match the guard's regexp. middie therefore runs zero middleware and Fastify's router — which keeps %2F encoded during lookup — still matches the route /user/:id/comments (with id = "a/b") and dispatches the handler. The guard is skipped.

The fix (commit 61d90cd, "fix(engine): preserve encoded slashes in middleware params", released as 9.3.3) replaces the decoder with find-my-way's safe decoder that preserves reserved characters such as %2F:

const { safeDecodeURI } = require('find-my-way/lib/url-sanitizer')
...
path = safeDecodeURI(path, options.useSemicolonDelimiter).path
path = decodeNestedPercentEncodedBytes(path)   // %25xx -> %xx only

With safeDecodeURI, /user/a%2Fb/comments stays /user/a%2Fb/comments for middleware matching, which matches /user/:id/comments, so the guard runs and blocks unauthenticated requests (401). Ordinary percent-encoded bytes and nested %25xx encodings remain compatible with previous matching behavior, and the malformed percent-encoding 400 handling is preserved.

  • Fix commit: 61d90cd0f578367283b486cb95f3b8c14bf3ddbf ("fix(engine): preserve encoded slashes in middleware params", v9.3.3).
  • Advisory ref: GHSA-2v46-jxjm-7q3v.

Reproduction Steps

  1. The self-contained script is bundle/repro/reproduction_steps.sh. It:
    • Reads bundle/project_cache_context.json and reuses the prepared project cache (repo-vuln-v932 = 9.3.2 vulnerable, repo = 9.3.3 fixed), with an npm install fallback to @fastify/middie@9.3.2 / @fastify/middie@9.3.3 if the cache is absent.
    • Registers a Fastify app with middie, an API-key auth guard on the parameterized middleware path /user/:id/comments, and a protected handler on the same pattern.
    • Exercises the library_api entrypoint via app.inject and a real HTTP server on 127.0.0.1 (raw node http client that preserves %2F) for both the vulnerable and the fixed build.
    • Asserts: vulnerable bypass → 200 (handler reached, guard bypassed); fixed bypass → 401 (guard matches); baseline → 401; allowed (with key) → 200 for both.
  2. Expected evidence: the vulnerable build returns 200 {"ok":true,"id":"a/b"} for the unauthenticated /user/a%2Fb/comments request, while the fixed build returns 401 {"error":"Unauthorized"}. A clean divergence proves the bypass and the patch.

Evidence

  • Master log: bundle/logs/reproduction_steps.log
  • Inject harness results: bundle/artifacts/inject_vuln.json, bundle/artifacts/inject_fixed.json (and bundle/logs/inject_vuln.log, bundle/logs/inject_fixed.log)
  • Real HTTP server evidence:
    • bundle/artifacts/http/vuln/server.log, bundle/artifacts/http/vuln/responses.txt
    • bundle/artifacts/http/fixed/server.log, bundle/artifacts/http/fixed/responses.txt
  • Runtime manifest: bundle/repro/runtime_manifest.json

Key excerpts (real HTTP server, raw node client preserving %2F):

=== vulnerable-server /user/a%2Fb/comments (NO api key) ===
STATUS:200
{"ok":true,"id":"a/b"}        <-- guard bypassed, protected handler reached

=== fixed-server /user/a%2Fb/comments (NO api key) ===
STATUS:401
{"error":"Unauthorized"}      <-- guard now matches and blocks

=== both builds baseline /user/alice/comments (NO api key) ===
STATUS:401  {"error":"Unauthorized"}   <-- guard works for normal paths

=== both builds /user/a%2Fb/comments (WITH api key) ===
STATUS:200  {"ok":true,"id":"a/b"}     <-- route still matches when allowed

Result summary from the script:

inject  vuln:  baseline=401 bypass=200 allowed=200
inject  fixed: baseline=401 bypass=401 allowed=200
server  vuln:  baseline=401 bypass=200
server  fixed: baseline=401 bypass=401

Environment: Node.js v24.18.0, @fastify/middie 9.3.2 (vulnerable) and 9.3.3 (fixed), Fastify from each workspace's node_modules. The vulnerable lib/engine.js uses FindMyWay.sanitizeUrlPath (decodes %2F); the fixed lib/engine.js uses safeDecodeURI (preserves %2F).

Recommendations / Next Steps

  • Upgrade to @fastify/middie@9.3.3 or later immediately. The fix preserves encoded slashes in middleware matching so parameterized guards can no longer be bypassed.
  • Audit existing middleware registrations: any guard on a parameterized path (/:param, /api/:resource, /user/:id/...) used for authn/authz/rate-limiting is a candidate bypass surface on vulnerable versions.
  • Defense in depth: do not rely solely on middleware for authorization; also enforce authorization inside route handlers, and normalize/reject encoded slashes at the edge where appropriate.
  • Regression test: the upstream fix ships test/security-encoded-slash-param-bypass.test.js; keep it in CI. Add cases for additional encodings (%2f lower-case, double-encoded %252F) and method-agnostic checks (POST/PUT/DELETE).

Additional Notes

  • Idempotency: reproduction_steps.sh was executed twice consecutively; both runs exited 0 with identical results (vulnerable bypass=200, fixed bypass=401). Servers are started on fixed localhost ports and torn down via trap/SIGTERM, so repeated runs are clean.
  • Two surfaces, one bug: the bypass is demonstrated both through the canonical library entrypoint (app.inject, classified as library_api to match the submitted claim surface) and over a real 127.0.0.1 TCP socket with a raw node http client that preserves %2F (curl --path-as-is was also verified to preserve %2F).
  • Limitations / edge cases: the bypass requires the guard to be registered on a parameterized path; a static-prefix guard (e.g. /api) is not bypassed by this specific vector. Lower-case %2f is equivalent to %2F for the decoder and is bypassed the same way. The malformed-percent (/%zz) 400 handling is preserved by the fix.

CVE-2026-14198 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:000:49
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-14198 · REPRO-20
0:02
0:03
web search
0:05
web search
0:07
0:08
0:10
0:11
web search
0:18
0:18
extract_facts
no facts extracted
0:20
0:20
0:20
supportrepro
0:34
0:35
0:35
0:35
0:37
0:37
0:43
0:43
0:43
$which node npm && node --version && npm --version
/usr/bin/node
/usr/bin/npm
v24.18.0
11.16.0
0:44
0:44
0:44
0:46
0:46
$cd /data/pruva/project-cache/b133492c-81d6-4c5b-afbf-03b28ec1f618/repo && git log --oneline -5 && echo "---PACKAGE---" && cat package.json | head -30 && echo "---ENGINE DIFF SIZE---" && wc -l lib/engine.js
{"cwd":"/data/pruva/runs/1097abc6-d12b-4882-a698-de4bec5dc5c5","duration_ms":7,"exit_code":0,"stderr":"","stdout":"e038188 Bumped v9.3.3\n61d90cd fix(engine): preserve encoded slashes in middleware params\n01acaed fix(engine): reject malformed percent-encoded paths\n378adeb chore: replace http with https in urls (#266)\n298a633 chore(package.json): fix delvedor's personal url\n---PACKAGE---\n{\n  \"name\": \"@fastify/middie\",\n  \"version\": \"9.3.3\",\n  \"description\": \"Middleware engine for Fastify\",\n  \"main\": \"index.js\",\n  \"type\": \"commonjs\",\n  \"types\": \"types/index.d.ts\",\n  \"scripts\": {\n    \"lint\": \"eslint\",\n    \"lint:fix\": \"eslint --fix\",\n    \"test\": \"npm run test:unit && npm run test:typescript\",\n    \"test:unit\": \"c8 --100 node --test\",\n    \"test:typescript\": \"tstyche\"\n  },\n  \"keywords\": [\n    \"fastify\",\n    \"middleware\",\n    \"webframework\",\n    \"performances\"\n  ],\n  \"author\": \"Matteo Collina <hello@matteocollina.com>\",\n  \"contributors\": [\n    {\n      \"name\": \"Tomas Della Vedova\",\n      \"url\": \"https://delvedor.dev\"\n    },\n    {\n      \"name\": \"Manuel Spigolon\",\n      \"email\": \"behem… [truncated]
0:46
$cd /data/pruva/project-cache/b133492c-81d6-4c5b-afbf-03b28ec1f618/repo-vuln-v932 && git log --oneline -5 && echo "---PACKAGE---" && cat package.json | head -30 && echo "---ENGINE---" && wc -l lib/engine.js
{"cwd":"/data/pruva/runs/1097abc6-d12b-4882-a698-de4bec5dc5c5","duration_ms":6,"exit_code":0,"stderr":"","stdout":"792d2f4 Bumped v9.3.2\nca42dd7 Merge commit from fork\n29162c8 Merge commit from fork\n33c7553 ci: add lock-threads workflow (#259)\ncaf538b build(deps-dev): bump neostandard from 0.12.2 to 0.13.0 (#258)\n---PACKAGE---\n{\n  \"name\": \"@fastify/middie\",\n  \"version\": \"9.3.2\",\n  \"description\": \"Middleware engine for Fastify\",\n  \"main\": \"index.js\",\n  \"type\": \"commonjs\",\n  \"types\": \"types/index.d.ts\",\n  \"scripts\": {\n    \"lint\": \"eslint\",\n    \"lint:fix\": \"eslint --fix\",\n    \"test\": \"npm run test:unit && npm run test:typescript\",\n    \"test:unit\": \"c8 --100 node --test\",\n    \"test:typescript\": \"tsd\"\n  },\n  \"keywords\": [\n    \"fastify\",\n    \"middleware\",\n    \"webframework\",\n    \"performances\"\n  ],\n  \"author\": \"Matteo Collina <hello@matteocollina.com>\",\n  \"contributors\": [\n    {\n      \"name\": \"Tomas Della Vedova\",\n      \"url\": \"http://delved.org\"\n    },\n    {\n      \"name\": \"Manuel Spigolon\",\n      \"email\": \"behemoth89@gmail.com\"\n    },\n---ENGINE---\n185 lib/engine.js\n","succ… [truncated]
0:49
08 · How to Fix

How to Fix CVE-2026-14198

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-14198 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-14198

How does the encoded-slash middleware bypass work?

An attacker sends a single request with an encoded slash embedded in a path-parameter position, such as /user/a%2Fb/comments where /user/:id/comments is protected by authentication, authorization, rate-limiting, or auditing middleware. Middie's decoded view of the path fails to match the guard and skips it, but Fastify's router still dispatches the undecoded path to the protected handler — the bypass works regardless of HTTP method.

How severe is CVE-2026-14198?

Critical — no authentication or preconditions are required, and any application using middie to guard a parameterized path (e.g. /api/:resource) can have that guard silently bypassed with one crafted URL.

How can I reproduce CVE-2026-14198?

Download the verified script from this page and run it in an isolated environment against a Fastify server using @fastify/middie 9.1.0-9.3.2 with an API-key guard on a parameterized route. It sends a request with an encoded slash in the path parameter and shows the protected handler responding without the middleware guard firing.
11 · References

References for CVE-2026-14198

Authoritative sources for CVE-2026-14198 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.