CVE-2026-14198: Verified Repro With Script Download
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.
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).
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 — 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 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00207/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-14198
- reached the target end-to-end
- on the real production code path
- high confidence
- the upstream fix blocks the same 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
- 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).
Negative variant result. Bounded, exhaustive search for a distinct variant or bypass of the @fastify/middie 9.3.3 fix for CVE-2026-14198 (encoded-slash authz bypass on parameterized middleware paths). Tested ~60 materially distinct candidate probes across encodings (%2F/%2f/mixed/multiple/%2F-only, double %252F, tripl…
How the agent worked
Root Cause and Exploit Chain for CVE-2026-14198
@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/commentsreturns 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 Fastifyapp.injectlibrary entrypoint and a real127.0.0.1HTTP 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
- The self-contained script is
bundle/repro/reproduction_steps.sh. It:- Reads
bundle/project_cache_context.jsonand reuses the prepared project cache (repo-vuln-v932= 9.3.2 vulnerable,repo= 9.3.3 fixed), with annpm installfallback to@fastify/middie@9.3.2/@fastify/middie@9.3.3if 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.injectand a real HTTP server on127.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.
- Reads
- Expected evidence: the vulnerable build returns
200 {"ok":true,"id":"a/b"}for the unauthenticated/user/a%2Fb/commentsrequest, while the fixed build returns401 {"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(andbundle/logs/inject_vuln.log,bundle/logs/inject_fixed.log) - Real HTTP server evidence:
bundle/artifacts/http/vuln/server.log,bundle/artifacts/http/vuln/responses.txtbundle/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.3or 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 (%2flower-case, double-encoded%252F) and method-agnostic checks (POST/PUT/DELETE).
Additional Notes
- Idempotency:
reproduction_steps.shwas 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 viatrap/SIGTERM, so repeated runs are clean. - Two surfaces, one bug: the bypass is demonstrated both through the canonical
library entrypoint (
app.inject, classified aslibrary_apito match the submitted claim surface) and over a real127.0.0.1TCP socket with a raw node http client that preserves%2F(curl--path-as-iswas 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%2fis equivalent to%2Ffor the decoder and is bypassed the same way. The malformed-percent (/%zz) 400 handling is preserved by the fix.
Variant Analysis & Alternative Triggers for CVE-2026-14198
No distinct variant or bypass of the @fastify/middie 9.3.3 fix was found. The
CVE-2026-14198 bypass (an unauthenticated attacker reaches a handler guarded by a
parameterized middie middleware path by embedding an encoded slash %2F in the
parameter position) was reproducible on the vulnerable 9.3.2 build across 24
distinct request shapes, but zero of those shapes — nor any of the additional
encoding, structural, router-option, HTTP-method, or encapsulated-prefix entry-point
variants tested — bypassed the fixed 9.3.3 build. The fix (commit 61d90cd,
"fix(engine): preserve encoded slashes in middleware params") aligns middie's
middleware-matching path decoder with Fastify's router decoder by switching from
FindMyWay.sanitizeUrlPath (which decoded %2F→/) to find-my-way's
safeDecodeURI (which preserves %2F), and adds a single-level
decodeNestedPercentEncodedBytes (%25XX→%XX) that can never re-introduce a
literal slash. Because there is only one path-decode sink in the codebase and the
fix replaced it wholesale, no alternate entry point or data path reaches a different
decoder. This is a negative variant result: the fix is complete for the
encoded-slash-in-parameter bypass class.
Fix Coverage / Assumptions
Invariant the fix relies on: the canonical request path used for middleware
matching must agree with the canonical request path used by Fastify's router
(find-my-way) for every input, so that a parameterized guard regexp compiled by
path-to-regexp matches iff the router dispatches the route. Concretely, reserved
path characters — above all the segment-breaking / (%2F/%2f) — must stay
encoded in middleware matching exactly as find-my-way keeps them encoded during
route lookup.
Code path(s) it explicitly covers: lib/engine.js → normalizePathForMatching().
This is the only function that decodes/normalizes the URL for middleware matching,
called once per request from run() (which is invoked from whatever hook middie is
registered on: onRequest default, or preValidation/preHandler/etc.). The fix:
- Replaces
path = FindMyWay.sanitizeUrlPath(path, useSemicolonDelimiter)(decodes%2F→/, introducing a phantom segment that breaks/user/:id/comments) withpath = safeDecodeURI(path, useSemicolonDelimiter).path(find-my-way's own decoder, which preserves%2F/%2fand other reserved chars while still decoding ordinary percent-encoded bytes so decoded middleware prefixes keep matching). - Adds
path = decodeNestedPercentEncodedBytes(path)—path.replace(/%25([0-9A-Fa-f]{2})/g, '%$1')— to keep nested-percent behavior compatible (e.g./%2565ncoded→/%65ncoded). This function only ever emits an encoded%XXtoken; it cannot emit a literal/, so it cannot re-introduce a segment-breaking slash. - (Companion commit
01acaed, same release) wraps the decoder intry/catchand returns{ path, error: FST_ERR_MIDDIE_MALFORMED_URL() }for malformed percent encoding (/%zz,/%), so the request is rejected with 400 instead of continuing with an invalid path;run()forwards that error toholder.done(error).
What the fix does NOT cover / gaps: None found for this bypass class. Every
materially distinct entry/data path tested is covered because all of them funnel
through the single normalizePathForMatching sink, which is now aligned with the
router. The only behavioral discrepancies the fix introduces (middie sees %2F
literal after decodeNestedPercentEncodedBytes of %252F, while the router sees
%252F literal) are non-segment-breaking on both sides, so the parameterized guard
regexp still matches on the middie side (the condition that determines whether the
guard runs).
Variant / Alternate Trigger
A bounded, exhaustive set of candidate variants was enumerated and tested on both
the vulnerable (9.3.2) and fixed (9.3.3 = latest published) builds. For each
candidate, a Fastify app registered an API-key guard on the parameterized path
/user/:id/comments and a handler on the same pattern; the candidate URL was injected
with and without the key. A bypass = noKey===200 && withKey===200 (guard skipped AND
route reachable).
Entry point(s) exercised (all library_api via app.inject, the canonical
Fastify entrypoint):
- Encoding variants against guard
/user/:id/comments:%2F; lowercase%2f; mixed case%2F..%2f; multiple%2Fin one param (a%2Fb%2Fc); param = only%2F(/user/%2F/comments); double-encoded%252F; double lowercase%252f; triple%25252F; two double%252F..%252F; quad%2525252F; bare%25;%252(one hex digit); with query?x=1; trailing slash/; semicolon;x; duplicate leading slash//. - Structural variants: multi-param guards
/api/:org/:repowith%2Fin the first and in the second param; prefix guard/files/:dir(end:false) + route/files/:dir/download; double-encoded%252Fin a multi-param guard. - Router-option combinations:
ignoreTrailingSlash,ignoreDuplicateSlashes,useSemicolonDelimiter, each alone and all combined, against the slash variants. - Method-agnosticism: GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS.
- Alternate registration entry point: guard registered inside an encapsulated
prefixed plugin (
fastify.register(plugin, { prefix: '/api' })→instance.use('/user/:id/comments', guard)), exercising theindex.jsprefix prepend path (this.prefix + path).
Code path(s) involved: lib/engine.js normalizePathForMatching() (the sole
decode sink), run()/Holder.done() (guard regexp match on normalizedUrl), and
index.js use()/resolveNormalizationOptions() (prefix + router-option
resolution). find-my-way lib/url-sanitizer.js safeDecodeURI() is the decoder both
layers now share.
Result — no variant/bypass on the fixed build. Consolidated counts across ~60
probes: vulnerable 9.3.2 → 24 bypasses; fixed 9.3.3 → 0 bypasses. The 24
vulnerable hits are exactly the single-encoded %2F shapes (incl. lowercase,
multiple, query, trailing-slash under ignoreTrailingSlash, dup-slash under
ignoreDuplicateSlashes, semicolon, all methods, encapsulated prefix). Notably,
double/triple/quad-encoded %252F… bypass NEITHER build — the vulnerable
sanitizeUrlPath is also single-pass, so %252F decodes once to literal %2F (not
/), and the guard matches on both. So nested encoding was never a bypass vector; the
fix's decodeNestedPercentEncodedBytes keeps that consistent rather than closing a
live hole.
- Package / component affected:
@fastify/middie(lib/engine.js,normalizePathForMatching). Single decode sink; fix replaced it wholesale. - Affected versions (as tested): vulnerable
9.3.2(commit792d2f46ae68516d3122c9a4468a5748a34efb47); fixed9.3.3(commite038188b33b9436e1be9f9d1c1920416ec6c18f1, includes fix61d90cd+ malformed-URL hardening01acaed).9.3.3is the latest published version (npm view @fastify/middie version→9.3.3), so the fixed build is also the latest. - Risk level / consequences (vulnerable): Critical — authn/authz (or rate-limit /
audit) bypass on any parameterized middleware guard with a static suffix after the
param (
/user/:id/comments,/api/:org/:repo/issues, etc.). Method-agnostic, no auth required. Risk level (fixed): None for this class — no bypass observed.
Impact Parity
- Disclosed / claimed maximum impact (parent): unauthenticated reach of a protected handler on a parameterized middleware path via one crafted URL; HTTP-method agnostic; no preconditions.
- Reproduced impact from this variant run: on the vulnerable build, the parent
bypass reproduces fully (24 shapes return
200 {"ok":true,"id":"a/b"}without the key, including all HTTP methods and the encapsulated-prefix entry point). On the fixed build, every candidate returns401 {"error":"Unauthorized"}without the key while the route remains reachable with the key (200) — i.e. the guard correctly blocks and the bypass does not reproduce. - Parity:
nonefor a new variant (no additional impact beyond the parent was demonstrated, because no fixed-build bypass exists). The parent bypass itself isfullparity on the vulnerable build. - Not demonstrated: no bypass on the patched/latest build; therefore no escalation, no RCE, no crash — none claimed and none reproduced.
Root Cause
The parent root cause: middie's normalizePathForMatching decoded %2F→/ (via
FindMyWay.sanitizeUrlPath) before matching middleware regexps, while
find-my-way's router preserved %2F during lookup. For a guard like
/user/:id/comments (param + static suffix, compiled with path-to-regexp
end:false), the decoded /user/a/b/comments has an extra segment and fails to match
the guard regexp (:id=a, then /b/comments ≠ /comments), so the guard is
skipped; the router still matches /user/:id/comments with id="a/b" and dispatches
the handler. The fix makes the two layers share the same decoder (safeDecodeURI), so
%2F stays encoded on both sides and the guard regexp matches.
Why no variant reaches the same bug on the fixed build: (1) There is exactly one
path-decode sink (normalizePathForMatching); the fix replaced it, so every entry
point (onRequest/preValidation/preHandler hooks, top-level use, encapsulated
prefixed use, all methods) funnels through the fixed decoder. (2) The only other
transform the fix adds, decodeNestedPercentEncodedBytes, maps %25XX→%XX and can
never produce a literal /, so it cannot recreate the segment-count mismatch that the
bypass requires. (3) Among reserved characters, only / is segment-breaking for
path-to-regexp; the fix preserves %2F/%2f, and ?/# are stripped pre-decode by
sanitizeUrl, while ; is handled identically by both layers via the shared
useSemicolonDelimiter flag. (4) The req.url stripping logic in Holder.done()
(origResult vs normalized fallback) runs after the guard-match decision and only
rewrites the remaining URL for downstream middleware; for parameterized guards the
origResult (match on the still-encoded sanitized URL) succeeds, so the decoded-path
fallback branch is not taken and cannot be used to slip past the guard.
Fix commit: 61d90cd0f578367283b486cb95f3b8c14bf3ddbf
("fix(engine): preserve encoded slashes in middleware params", released as 9.3.3).
Companion: 01acaed3b2353aef4611cd534b6a7267ca215227 (reject malformed percent
encoding). Advisory: GHSA-2v46-jxjm-7q3v.
Reproduction Steps
- The self-contained script is
bundle/vuln_variant/reproduction_steps.sh. It:- Resolves the vulnerable (9.3.2) and fixed (9.3.3)
@fastify/middieworkspaces (project cache preferred,npm installfallback), ensuringfastifyis present. - Embeds a consolidated Node probe harness and runs it against both builds. The
harness registers a parameterized guard
/user/:id/comments+ handler and injects every candidate URL with and without the API key, recordingnoKeyStatus,withKeyStatus, matched params, and abypassflag. - Counts bypasses per build, verifies the control (
%2Fbypasses vulnerable=true, fixed=false → harness valid), writes a comparison table and a runtime manifest. - Exit 0 = a bypass was found on the FIXED build (variant confirmed); exit 1 = no bypass on the fixed build (negative result). This run exits 1.
- Resolves the vulnerable (9.3.2) and fixed (9.3.3)
- Expected evidence:
vulnerable 9.3.2 → 24 bypasses;fixed 9.3.3 → 0 bypasses; control%2F→ vulnerable200, fixed401. The full per-candidate table is inbundle/logs/vuln_variant/consolidated_comparison.txt.
Evidence
- Master log:
bundle/logs/vuln_variant/reproduction_steps.log - Consolidated comparison table:
bundle/logs/vuln_variant/consolidated_comparison.txt(alsobundle/vuln_variant/out/comparison.txt) - Raw probe JSON (both builds):
bundle/logs/vuln_variant/probe_vuln.json,bundle/logs/vuln_variant/probe_fixed.json - Runtime manifest:
bundle/vuln_variant/runtime_manifest.json - Probe harness (embedded copy):
bundle/vuln_variant/harness_gen/consolidated_probe.js(standalone exploratory harnesses also underbundle/vuln_variant/harness/)
Key excerpts (fixed build, no-key requests — all blocked):
standard | control_original_%2F | /user/a%2Fb/comments | 401/200 | fixedBypass=false
standard | lowercase_%2f | /user/a%2fb/comments | 401/200 | fixedBypass=false
standard | double_%252F | /user/a%252Fb/comments | 401/200 | fixedBypass=false
standard | triple_%25252F | /user/a%25252Fb/comments | 401/200 | fixedBypass=false
standard | two_single_%2F_in_param | /user/a%2Fb%2Fc/comments | 401/200 | fixedBypass=false
opts:all | ignoreTrailing_%2F | /user/a%2Fb/comments/ | 401/200 | fixedBypass=false
methods | method_bypass_%2F (POST) | /user/a%2Fb/comments | 401/200 | fixedBypass=false
prefix | prefix_%2F | /api/user/a%2Fb/comments | 401/200 | fixedBypass=false
SUMMARY: vulnerable bypasses=24 fixed bypasses=0
VERDICT: NO BYPASS on fixed build across all candidate variants.
(Vulnerable build, same requests — bypassed: /user/a%2Fb/comments no-key →
200 {"ok":true,"id":"a/b"} across GET/POST/PUT/PATCH/DELETE/HEAD and the
encapsulated prefix.)
Environment: Node.js v24.18.0; @fastify/middie 9.3.2
(792d2f46…) and 9.3.3 (e038188b…, latest published); find-my-way 9.6.0 in both
workspaces; fastify from each workspace's node_modules.
Recommendations / Next Steps
The 9.3.3 fix is complete for the encoded-slash-in-parameter bypass class; no
code change is required to close a gap (none was found). Recommended regression
hardening to lock in the coverage that the existing
test/security-encoded-slash-param-bypass.test.js does not yet exercise:
- Add param-position cases for lowercase
%2f, multiple%2Fin one param, and param = only%2F(/user/%2F/comments). - Add double/triple-encoded
%252F/%25252Fparam cases (assert the guard blocks on the fixed build AND that these were never bypass vectors on the vulnerable build, documenting the single-pass decoder behavior). - Add method-agnostic cases: register the route for POST/PUT/DELETE/PATCH and
assert the guard blocks the
%2Fvariant for each method (the CVE states the bypass is method-agnostic; the test currently only covers GET). - Add an encapsulated-prefix case (
register(plugin, { prefix })+instance.use('/user/:id/comments', guard)) to cover theindex.jsprefix-prepend registration path. - Add
ignoreTrailingSlash+ignoreDuplicateSlashes+useSemicolonDelimiterparam-position combinations (the existingsecurity-router-options-combinations.test.jscovers these only for the static/secretguard, not for a parameterized guard with a static suffix).
Defense-in-depth (unchanged from the parent RCA): do not rely solely on middleware for authorization; also enforce authorization inside route handlers, and normalize/reject encoded slashes at the edge where appropriate.
Additional Notes
- Idempotency:
reproduction_steps.shwas executed multiple times consecutively; every run exited 1 with identical results (vulnerable 24 / fixed 0). Output files are overwritten each run; no repo checkout state is mutated (read-only use of the project cache workspaces; the script nevergit checkouts). - Fixed = latest:
npm view @fastify/middie versionreturns9.3.3, so the fixed build tested here is also the latest published release; the mandatory fixed-version and latest-version checks are satisfied by the same build. - Trust boundary / threat model: there is no
SECURITY.mdin the repo; the advisory GHSA-2v46-jxjm-7q3v establishes that authn/authz bypass on parameterized middleware paths is in-scope and was patched. All candidate variants cross the same trust boundary (untrusted HTTP → Fastify server using a middie guard on a parameterized path) as the parent. - Scope nuance observed: the bypass requires the parameterized guard to have a
static suffix after the param (
/user/:id/comments). A prefix-only guard (/user/:id,end:false) is not bypassed even on the vulnerable build, because the decoded extra slash still matches the open-ended prefix. This is a scoping detail of the same root cause, not a distinct variant. - Limitations: tested via
app.inject(library_api). The parent repro already demonstrated the same bypass over a real127.0.0.1TCP socket with a raw node http client that preserves%2F; the fix is transport-agnostic (it is in the path normalization, before any I/O layer), so the negative result transfers to the HTTP server surface.
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.
which node npm && node --version && npm --version/usr/bin/node /usr/bin/npm v24.18.0 11.16.0
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]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]Artifacts and Evidence for CVE-2026-14198
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-14198
FAQ: CVE-2026-14198
How does the encoded-slash middleware bypass work?
How severe is CVE-2026-14198?
How can I reproduce CVE-2026-14198?
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.