Someone left the SVG door wide open and an attacker walked straight into your server room
CVE-2026-94545 is an unauthenticated remote code execution vulnerability in Vercel's Satori library (versions ≥0.0.27, <0.33.5), which Next.js bundles inside its next/og ImageResponse API. Satori converts JSX and CSS into SVG, but it writes user-supplied text and attribute values into the SVG output without escaping them (CWE-116). An attacker who can reach an OG-image route that reflects query-parameter data — the single most common OG image pattern — can close the surrounding SVG element and inject arbitrary markup. When the route runs on the Node.js runtime (the default) with sharp installed, the injected SVG is rasterised by native C libraries (libvips → librsvg → libxml2). A payload containing XInclude references and nested DTD entities triggers memory corruption inside libxml2. Because the official Node.js binary is non-PIE, ASLR does not randomise its code segment, so a fixed ROP chain calls execve without needing an address leak. Affected versions: Next.js 16.2.0–16.3.5, Next.js 15.x < 15.5.26 (backport), and standalone Satori 0.0.27–0.33.4. Edge runtime is not affected because it uses a sandboxed WASM renderer instead of native libraries.
The severity picture is a mess. The CNA scored Satori itself at CVSS v4 5.3 (Medium) using a vector that treats the impact as hitting a 'subsequent system' and requires passive user interaction — an interpretation that badly misrepresents a server-side RCE triggered by a single unauthenticated HTTP request. Vercel's own Next.js GHSA scored the same CVE at CVSS v4 9.8 (Critical), and some aggregators report v3.1 scores anywhere from 6.1 to 10.0. Neither extreme is right. The CNA vector undercounts because the RCE occurs on the vulnerable system, not a subsequent one, and no user interaction is needed; the 9.8 overcounts because exploitation requires the app to opt into next/og, run on Node.js runtime, have sharp installed, and reflect user input — a common but not universal pattern. noisgate assesses this at HIGH / 8.1 after friction adjustment.
5 steps from start to impact.
Discover OG image endpoint
/_next/ paths, page source) and probes for dynamic OG-image routes — typically /api/og, /og, or any route exporting an ImageResponse. These endpoints are publicly reachable by design because social-media crawlers must fetch them without authentication.- Target runs Next.js 16.2.0–16.3.5 or 15.x < 15.5.26
- Application exposes at least one
ImageResponseroute
- Not every Next.js app uses
next/og— it is an opt-in feature for dynamic Open Graph images - Some deployments serve only static pre-built OG images, which do not invoke Satori at request time
Inject SVG markup via query parameter
</text>) and opening a new one. The most common exploitable pattern is an OG route that reads searchParams.title or searchParams.description and interpolates the value into JSX rendered by ImageResponse. Weaponised tool: EQSTLab/CVE-2026-94545 exploit.py automates payload construction.- At least one user-controlled value (query param, path segment, header) reaches an SVG text node, attribute, or style in the ImageResponse JSX
- Applications that only render hardcoded or CMS-sourced trusted content are not reachable
- Some frameworks sanitise query params at the middleware layer before they reach the route handler
</, <svg, <foreignObject, xi:include) in query parameters to image-generation endpointsSatori serialises unescaped payload into SVG
renderToSVG function concatenates user-supplied values directly into the SVG document without escaping <, >, ", &, or XML name characters. The injected markup becomes part of a well-formed SVG document that Satori returns. This is the root cause — CWE-116 (Improper Encoding or Escaping of Output).- Satori version ≥0.0.27, <0.33.5
- None — if Step 2 succeeds, Satori will faithfully serialize the injection
Native parser processes malicious XInclude/DTD entities
sharp for PNG rasterisation. sharp delegates to libvips, which calls librsvg, which calls libxml2 to parse the SVG/XML. The attacker's payload includes XInclude references and nested DTD entity definitions that trigger a heap buffer overflow in libxml2's entity expansion code. Applications using the Edge runtime or the WASM-based @vercel/og renderer skip this native path entirely and are not vulnerable to RCE.- Route runs on Node.js runtime (the default, not Edge)
- sharp npm package is installed (standard for Next.js image optimisation)
- Host has libvips/librsvg/libxml2 native libraries (installed automatically with sharp)
- Deployments on Vercel Edge Functions or Cloudflare Workers use the WASM renderer and skip the native library chain
- Containerised deployments using minimal base images may lack the specific libxml2 build targeted by the ROP chain, though most do include it
node process spawning unexpected child processes (e.g. sh, bash, curl, wget); EDR telemetry on native library crash/fault signalsROP chain → execve → remote shell
.text segment and GOT are loaded at fixed addresses across all hosts running the same Node build. The attacker's payload contains a pre-built ROP chain using gadgets from the Node binary itself, ending in an execve("/bin/sh", …) syscall. No address leak is needed — a single HTTP request achieves full RCE as the Node.js process user. Weaponised tool: EQSTLab/CVE-2026-94545 includes the complete ROP payload.- Node.js binary is non-PIE (true for official builds from nodejs.org and most distro packages)
- libxml2 heap layout is predictable (default allocator, no hardened malloc)
- Custom Node.js builds compiled with
-piewould randomise gadget addresses, but this is extremely rare in production - glibc heap hardening (e.g.
MALLOC_CHECK_, safe-linking in glibc ≥2.32) may corrupt the chain, but the PoC accounts for common glibc versions
execve from a Node.js worker, outbound reverse-shell connections, or anomalous file writes by the node processexport const runtime = 'edge' to every route file that uses ImageResponse. The Edge runtime uses a sandboxed WASM-based SVG renderer (@vercel/og) instead of the native sharp → libvips → libxml2 chain, completely eliminating the RCE path. This is the single most effective mitigation and can be deployed in minutes. Per the noisgate mitigation SLA for HIGH, deploy within 30 days — though given the public exploit availability, same-week deployment is strongly recommended.npm install [email protected] (or [email protected] for v15 branches). This escapes text and attribute values before SVG insertion, blocking the injection at the Satori layer. Per the noisgate remediation SLA for HIGH, complete within 180 days. ⚠ Note: [email protected] has reported bypass vectors (issue #826) — combine with Edge runtime or input controls until a complete fix ships.</, <svg, <foreignObject, xi:include, <!ENTITY, <!DOCTYPE. This is a defense-in-depth layer — not a substitute for patching. It stops the known exploit payloads but may be bypassable with encoding tricks.execve("/bin/sh") fails if /bin/sh does not exist or the seccomp profile blocks the syscall. Deploy within 30 days per noisgate mitigation SLA.- Input sanitisation/validation in application code — the escaping failure is in Satori's SVG serialisation, not in how the application passes data to ImageResponse. Even if you HTML-encode your inputs, Satori may decode or re-process them during rendering.
- Content Security Policy (CSP) headers — CSP is a browser-side control. This vulnerability is server-side RCE; the malicious SVG never reaches a browser.
- Rate limiting on OG image endpoints — the exploit requires exactly one HTTP request to achieve code execution. Rate limiting provides no meaningful protection.
- Upgrading only Satori to 0.33.5 without runtime change — GitHub issue #826 reports 3 confirmed bypass vectors in the 0.33.5 fix. SVG animation elements bypass both
escape-htmlandassertValidXMLName. The fix is incomplete as a standalone measure.
The supporting signals.
| In-the-wild exploitation | No confirmed attacks as of 2026-10-09. Not listed in CISA KEV. SOCPrime and GreyNoise have not reported scanning activity targeting this CVE specifically. However, technical details and full exploit code are public, making weaponisation trivial. |
|---|---|
| Proof-of-concept availability | Multiple public PoCs. EQSTLab/CVE-2026-94545 — full exploit script (exploit.py) with ROP payload, demonstrated root shell on test container. Hassham1/CVE-2026-94545-nextjs-og-poc — Docker validation lab with vulnerable (16.3.5) vs patched (16.3.6) controls and Python scanner. Also indexed: mhtsec/CVE-2026-94545, MRdark-ops/CVE-2026-94545- on Sploitus. |
| EPSS score | 0.00796 (0.796%) — moderate for a 9-day-old CVE. Given the public PoC availability and Next.js install base, expect this to climb in coming weeks. |
| KEV status | Not listed. No CISA Known Exploited Vulnerabilities entry as of 2026-10-09. |
| CVSS vectors (conflicting) | CNA (Satori GHSA-wx4j-mvgx-mqwp): CVSS:4.0 5.3 Medium — AV:N/AC:L/AT:P/PR:N/UI:P/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N. Vercel Next.js GHSA-vcvr-r3jv-pc5j: CVSS:4.0 9.8 Critical. CVSS v3.1 reported as 6.1 (CNA) to 10.0 (some aggregators). The CNA vector misclassifies this as a subsequent-system impact with user interaction — both wrong for server-side RCE via a single unauthenticated request. |
| Affected version ranges | Satori ≥0.0.27, <0.33.5. Next.js ≥16.2.0, <16.3.6. Next.js 15 — backport fix at 15.5.26 implies 15.x was also affected via bundled Satori. Edge runtime is not affected regardless of version. |
| Fixed versions | Satori 0.33.5, Next.js 16.3.6, Next.js 15.5.26 (backport). ⚠ Note: GitHub issue #826 reports 3 bypass vectors in satori@0.33.5 — the fix may be incomplete. SVG animation elements reportedly bypass both escape-html and assertValidXMLName. |
| Installed base / exposure | Next.js powers ~2.7M websites (Wappalyzer, Oct 2026) — 3.5% of all websites (W3Techs). The subset using next/og ImageResponse with dynamic user input is smaller but represents a significant population of marketing, SaaS, and content sites. Shodan/Censys can fingerprint Next.js via x-powered-by and /_next/ paths. |
| Disclosure timeline | 2026-09-22: Vercel publishes security advisory and releases Next.js 16.3.6. 2026-09-30: CVE-2026-94545 published. 2026-10-01+: EQSTLab and others publish full exploit code. |
| Reporting researcher | Reported by Raghav Maheshwari and rafabd1 via Vercel's security process. EQSTLab published the first public full-chain exploit. Incomplete-fix bypass reported by an independent researcher via vercel/satori#826. |
Sources.
- GHSA-vcvr-r3jv-pc5j — Next.js ImageResponse RCE Advisory
- OSV — CVE-2026-94545 Vulnerability Detail
- EQSTLab/CVE-2026-94545 — Full Exploit PoC (GitHub)
- SecurityOnline — PoC Exploit Released for Next.js RCE Flaw
- SOCPrime — CVE-2026-94545 Next.js ImageResponse RCE Analysis
- Incomplete Fix — vercel/satori Issue #826
- ByteIota — CVE-2026-94545 Patch to Next.js 16.3.6 Analysis
- Tenable — CVE-2026-94545
Why this verdict
- Unauthenticated RCE from the internet: The full chain — from a single crafted HTTP request to
execveon the server — requires no authentication, no user interaction, and no prior foothold. The non-PIE Node.js binary eliminates ASLR, making exploitation deterministic with the published ROP chain. - Massive install base with common exposure pattern: Next.js runs on ~2.7M websites. The dominant use case for
next/ogis exactly the vulnerable pattern: reading a page title or description from URL query parameters and rendering it into an OG image. This is the pattern shown in Vercel's own documentation and tutorials. - Multiple weaponised PoCs available: At least four independent GitHub repositories provide exploit code, including EQSTLab's full ROP-chain exploit that demonstrated root shell access. The barrier to exploitation is essentially zero for any attacker who can identify a vulnerable endpoint.
- Friction narrows the reachable population: Not all Next.js apps use
next/og(opt-in feature). Edge runtime deployments are immune. Apps rendering only trusted/hardcoded content are not exploitable. These conditions reduce the vulnerable population to a significant but bounded subset. - Role multiplier: Next.js is an application-tier web framework — its typical deployment role is a line-of-business web server (role b). It is NOT canonically a high-value-role component (not an IdP, DC, hypervisor, backup system, or network edge appliance). However, production Next.js servers are internet-facing and typically hold database credentials, API keys, session secrets, and cloud metadata access. Compromise of a production web server enables host-level data exfiltration and lateral movement into backend services. For enterprises running Next.js as their primary customer-facing application (e-commerce, SaaS portals), blast radius reaches tenant-level data exposure. The chain succeeds fully in the typical role — the blast radius is host → application data → lateral movement, which justifies HIGH but does not trigger the CRITICAL floor since <10% of Next.js installs occupy a catalog high-value role.
- No confirmed wild exploitation: Despite public PoCs, no confirmed attacks have been observed (not KEV-listed, no GreyNoise tags). EPSS at 0.796% is moderate. This prevents an upward push to CRITICAL.
Why not higher?
This is not CRITICAL because Next.js is an application-tier framework, not a canonical high-value-role component. The vulnerability requires the app to opt into next/og, run on Node.js runtime (not Edge), have sharp installed, and reflect user-controlled input into ImageResponse — a common but not universal deployment pattern. There is no confirmed wild exploitation yet, and EPSS remains moderate at 0.796%. The floor rule does not trigger because <10% of Next.js installations occupy high-value-role positions from the catalog.
Why not lower?
Downgrading below HIGH would be irresponsible. This is a deterministic, unauthenticated RCE from the internet with zero user interaction, multiple public full-chain exploits, and a fixed ROP chain that works on any host running the same Node.js build. The affected framework powers 2.7 million websites, and the vulnerable code path is the most commonly documented OG-image implementation pattern. The incomplete fix in [email protected] (3 confirmed bypass vectors) adds further urgency. Any enterprise running Next.js 16.2–16.3.5 with dynamic OG images must treat this as an immediate priority.
Crowdsourced verification payload.
Run this script from the root directory of each Next.js project on the application host (or in your CI pipeline). Requires node in PATH. No elevated privileges needed — it reads package.json files from node_modules. Example: cd /srv/myapp && bash cve-2026-94545-check.sh
#!/usr/bin/env bash
# CVE-2026-94545 — Satori / Next.js ImageResponse SVG Injection RCE
# Outputs: VULNERABLE / PATCHED / UNKNOWN
# Exit codes: 0=PATCHED 1=VULNERABLE 2=UNKNOWN
set -uo pipefail
STATUS="UNKNOWN"
DETAILS=""
version_lt() {
[ "$(printf '%s\n' "$1" "$2" | sort -V | head -n1)" = "$1" ] && [ "$1" != "$2" ]
}
# --- Check Next.js version ---
if [ -f node_modules/next/package.json ]; then
NEXT_VER=$(node -p "require('./node_modules/next/package.json').version" 2>/dev/null || true)
if [ -n "$NEXT_VER" ]; then
IFS='.' read -r MAJ MIN PAT <<< "$NEXT_VER"
if [ "$MAJ" -eq 16 ] 2>/dev/null; then
if [ "$MIN" -eq 2 ] || { [ "$MIN" -eq 3 ] && [ "$PAT" -lt 6 ]; }; then
STATUS="VULNERABLE"
DETAILS="Next.js $NEXT_VER in affected range 16.2.0-16.3.5"
else
STATUS="PATCHED"
DETAILS="Next.js $NEXT_VER outside vulnerable range"
fi
elif [ "$MAJ" -eq 15 ] 2>/dev/null; then
if version_lt "$NEXT_VER" "15.5.26"; then
STATUS="VULNERABLE"
DETAILS="Next.js $NEXT_VER needs backport >=15.5.26"
else
STATUS="PATCHED"
DETAILS="Next.js $NEXT_VER includes backported fix"
fi
else
STATUS="PATCHED"
DETAILS="Next.js $NEXT_VER not in affected major version"
fi
fi
fi
# --- Check standalone Satori version ---
if [ -f node_modules/satori/package.json ]; then
SATORI_VER=$(node -p "require('./node_modules/satori/package.json').version" 2>/dev/null || true)
if [ -n "$SATORI_VER" ]; then
if version_lt "$SATORI_VER" "0.33.5" && ! version_lt "$SATORI_VER" "0.0.27"; then
STATUS="VULNERABLE"
DETAILS="${DETAILS:+$DETAILS; }Satori $SATORI_VER in affected range 0.0.27-0.33.4"
elif [ "$STATUS" != "VULNERABLE" ]; then
STATUS="PATCHED"
DETAILS="${DETAILS:+$DETAILS; }Satori $SATORI_VER patched"
fi
fi
fi
# --- Exposure indicators ---
if [ "$STATUS" = "VULNERABLE" ]; then
if grep -rql 'next/og\|ImageResponse' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' . 2>/dev/null; then
DETAILS="$DETAILS | next/og ImageResponse USAGE DETECTED in source"
fi
if [ -d node_modules/sharp ]; then
DETAILS="$DETAILS | sharp installed (full RCE chain reachable)"
fi
fi
echo "$STATUS — $DETAILS"
case "$STATUS" in
VULNERABLE) exit 1 ;;
PATCHED) exit 0 ;;
*) echo "Neither Next.js nor Satori found in $(pwd)"; exit 2 ;;
esac