← Back to Feed CACHED · 2026-10-09 02:48:09 · CACHE_KEY CVE-2026-94545
CVE-2026-94545 · CWE-116 · Disclosed 2026-09-30

Satori SVG-Serialization Markup Injection leading to RCE in Next.js ImageResponse

ASSESSED — NOISGATE
Vendor
—
—
—
Reassessed
—
—
—
Verdict: —
Do you agree?
01 · The Real Story

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.

"One crafted URL to your OG image route gives an attacker a shell on your Node.js server."
02 · The Attack Path

5 steps from start to impact.

STEP 01

Discover OG image endpoint

The attacker fingerprints the target as a Next.js application (response headers, /_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.
Conditions required:
  • Target runs Next.js 16.2.0–16.3.5 or 15.x < 15.5.26
  • Application exposes at least one ImageResponse route
Where this breaks in practice:
  • 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
Detection/coverage: Web application scanners (Nuclei template exists); passive header fingerprinting with Wappalyzer or BuiltWith
STEP 02

Inject SVG markup via query parameter

The attacker crafts a URL whose query-string parameters contain SVG markup — closing the current element (e.g. </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.
Conditions required:
  • At least one user-controlled value (query param, path segment, header) reaches an SVG text node, attribute, or style in the ImageResponse JSX
Where this breaks in practice:
  • 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
Detection/coverage: WAF rules matching XML/SVG metacharacters (</, <svg, <foreignObject, xi:include) in query parameters to image-generation endpoints
STEP 03

Satori serialises unescaped payload into SVG

Satori's 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).
Conditions required:
  • Satori version ≥0.0.27, <0.33.5
Where this breaks in practice:
  • None — if Step 2 succeeds, Satori will faithfully serialize the injection
Detection/coverage: SAST/SCA scanners flagging vulnerable Satori versions (Snyk, Checkmarx SCA, GitHub Dependabot)
STEP 04

Native parser processes malicious XInclude/DTD entities

The generated SVG is passed to 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.
Conditions required:
  • 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)
Where this breaks in practice:
  • 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
Detection/coverage: Runtime detection: monitor for node process spawning unexpected child processes (e.g. sh, bash, curl, wget); EDR telemetry on native library crash/fault signals
STEP 05

ROP chain → execve → remote shell

Because the official Node.js binary is compiled as non-PIE (Position-Independent Executable), its .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.
Conditions required:
  • 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)
Where this breaks in practice:
  • Custom Node.js builds compiled with -pie would 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
Detection/coverage: EDR/HIDS alerting on execve from a Node.js worker, outbound reverse-shell connections, or anomalous file writes by the node process
03 · Compensating Control

1
HIGH 8.1→LOW 3.1
SEVERITY REDUCED
Switch affected ImageResponse routes to Edge runtime — Add export 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.
2
HIGH 8.1→MEDIUM 4.5
SEVERITY REDUCED
Upgrade to Next.js 16.3.6 / 15.5.26 and Satori 0.33.5 — Apply the vendor patch: 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.
3
HIGH 8.1→LOW 2.5
SEVERITY REDUCED
Remove or gate user-controlled input from ImageResponse — If you cannot switch runtimes immediately, refactor OG-image routes to use only server-side data (CMS content, database records) rather than reflecting query parameters, path segments, or headers. An allowlist of known-safe values is acceptable; a blocklist is not. This breaks Step 2 of the attack path by removing attacker-controlled data from the SVG pipeline.
4
HIGH 8.1→MEDIUM 5.0
SEVERITY REDUCED
Deploy WAF rules blocking SVG/XML injection in OG image parameters — Configure your WAF (Cloudflare, AWS WAF, Fastly, Akamai) to block or sanitise requests to OG-image endpoints containing XML/SVG metacharacters: </, <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.
5
HIGH 8.1→MEDIUM 5.5
SEVERITY REDUCED
Containerise Next.js with minimal base image and non-root user — Run the Next.js process in a read-only container with no shell binaries, a non-root user, dropped capabilities, and a restrictive seccomp profile. This does not prevent the memory corruption but limits post-exploitation impact — the attacker's 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.
What doesn't work
  • 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-html and assertValidXMLName. The fix is incomplete as a standalone measure.
04 · Intelligence Metadata

The supporting signals.

In-the-wild exploitationNo 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 availabilityMultiple 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 score0.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 statusNot 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 rangesSatori ≥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 versionsSatori 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 / exposureNext.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 timeline2026-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 researcherReported 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.

  1. GHSA-vcvr-r3jv-pc5j — Next.js ImageResponse RCE Advisory
  2. OSV — CVE-2026-94545 Vulnerability Detail
  3. EQSTLab/CVE-2026-94545 — Full Exploit PoC (GitHub)
  4. SecurityOnline — PoC Exploit Released for Next.js RCE Flaw
  5. SOCPrime — CVE-2026-94545 Next.js ImageResponse RCE Analysis
  6. Incomplete Fix — vercel/satori Issue #826
  7. ByteIota — CVE-2026-94545 Patch to Next.js 16.3.6 Analysis
  8. Tenable — CVE-2026-94545
05 · The Call

Final Verdict
= UNCHANGED to HIGH (8.1/10)

Why this verdict

  • Unauthenticated RCE from the internet: The full chain — from a single crafted HTTP request to execve on 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/og is 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.

06 · Verification

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

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/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