Sep 30, 2026·7 min read·6 visits
Unsanitized user inputs passed to the Next.js `ImageResponse` wrapper are processed by the underlying Satori library without appropriate HTML/XML escaping. This allows attackers to break out of XML tag boundaries, inject arbitrary SVG nodes, and trigger downstream vulnerabilities in image-processing backends.
An improper output encoding and escaping vulnerability (CWE-116) in Vercel Satori allows unauthenticated remote attackers to perform markup injection in dynamic Open Graph images generated via Next.js's ImageResponse. Unsanitized parameter interpolation into SVG elements breaks XML structural boundaries. This exposes downstream parsing, rasterization, and rendering pipelines to Server-Side Request Forgery (SSRF), Local File Read, and Remote Code Execution (RCE).
The Vercel Satori library is a widely adopted utility designed to convert HTML and CSS layouts into vector-based SVG graphics. In modern web architectures, particularly within the Next.js ecosystem, this functionality is wrapped by the next/og ImageResponse API. It enables developers to generate dynamic Open Graph (OG) social card images on-demand. Because these endpoints accept user-controlled query parameters to customize the text and layout of the generated cards, they present a high-exposure attack surface.
Historically, security models assumed that the JSX-to-SVG conversion pipeline handled input sanitization implicitly. However, CVE-2026-94545 exposes a failure in this pipeline. The vulnerability is classified under CWE-116 (Improper Encoding or Escaping of Output). It allows an attacker to manipulate the structural integrity of the generated XML document, escaping defined text fields and introducing arbitrary vector elements.
When a vulnerable Next.js application hosts an active Node.js-based ImageResponse endpoint, a remote, unauthenticated attacker can supply crafted query parameters containing XML tags. Satori incorporates these inputs into its output string without appropriate sanitization. This structural breakdown allows attackers to target downstream PDF or PNG rendering engines, which often run in highly privileged internal network environments.
The structural flaw originates within Satori's serialization routine, specifically inside the translateSVGNodeToSVGString() function located in src/handler/preprocess.ts. Satori transforms incoming React-like JSX nodes into string-based SVG markup. During this process, elements were systematically interpolated using standard ES6 template literals instead of structured, schema-validated XML node construction.
In affected versions, Satori converted raw text nodes directly by executing String(node) and appending the result directly to the output. If the input contained valid XML control characters, such as </title>, the text node prematurely closed its parent element and initiated a new, attacker-controlled XML context. Satori failed to run an HTML/XML entity encoder on these variables before concatenation.
Furthermore, Satori processed attributes and inline styles unsafely. Attribute values were encapsulated inside double quotes but were not escaped to prevent double-quote breakouts. In addition, internal layout properties starting with an underscore character, such as _inheritedBackgroundClipTextPath, were exposed to style interpolation. Attackers could manipulate these properties to alter internal document layouts or inject inline elements.
Finally, when Satori generated embedded SVG elements within HTML data URLs, it applied a restrictive regex filter (/[ %#()<>?[\n]^{|}"']/g) to encode special characters. This encoding lookup omitted the ampersand character. As a result, XML entities like < or external entity declarations (&entity;`) bypassed the encoder, enabling raw XML injection in nested rendering contexts.
To resolve the structural breakout, the Satori development team completely refactored the XML generation engine. They replaced template-literal concatenation with a strict, validation-driven XML builder pattern.
// Pre-Patch (Vulnerable Concatenation Loop in src/handler/preprocess.ts)
// Unescaped text node concatenation
if (typeof node !== 'object') return String(node);
// Unsafely interpolated attributes
let attrString = '';
for (const [key, value] of Object.entries(attrs)) {
attrString += ` ${key}="${value}"`;
}
return `<${type}${attrString}>${children}</${type}>`;// Post-Patch (Secured Validation and Serialization in src/utils.ts)
import escapeHTML from 'escape-html';
// 1. Strict validation of tag and attribute names to prevent parameter pollution
const XML_NAME_START_CHAR =
':A-Z_a-z\\u00C0-\\u00D6' +
'\\u00D8-\\u00F6\\u00F8-\\u02FF' +
'\\u0370-\\u037D\\u037F-\\u1FFF' +
'\\u200C-\\u200D\\u2070-\\u218F' +
'\\u2C00-\\u2FEF\\u3001-\\uD7FF' +
'\\uF900-\\uFDCF\\uFDF0-\\uFFFD';
const XML_NAME_CHAR =
XML_NAME_START_CHAR + '\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040';
const VALID_XML_NAME = new RegExp(
'^[' + XML_NAME_START_CHAR + '][' + XML_NAME_CHAR + ']*$',
'u'
);
export function assertValidXMLName(name: string, kind: 'element' | 'attribute') {
if (!VALID_XML_NAME.test(name)) {
throw new Error(`Invalid XML ${kind} name: ${JSON.stringify(name)}`);
}
}
// 2. Context-aware escaping utilities
export function escapeXMLAttribute(value: unknown): string {
if (typeof value === 'number' || typeof value === 'boolean') {
return '' + value;
}
return escapeHTML(String(value));
}
export function escapeXMLText(value: unknown): string {
return escapeHTML(String(value));
}
// 3. Structured builder replacing template strings
export function buildXMLString(type: string, attrs: Record<string, unknown>, children?: string) {
assertValidXMLName(type, 'element');
let attrString = '';
for (const [k, v] of Object.entries(attrs)) {
if (typeof v !== 'undefined') {
assertValidXMLName(k, 'attribute');
attrString += ` ${k}="${escapeXMLAttribute(v)}"`;
}
}
if (typeof children !== 'undefined') {
return `<${type}${attrString}>${children}</${type}>`;
}
return `<${type}${attrString}/>`;
}Additionally, the patch introduced an internal property check in Satori's style processing module (src/handler/expand.ts) that rejects keys prefixed with an underscore. The regex for embedded SVG symbols was updated to include the ampersand character, neutralizing entity-based injection vectors.
An analysis of the exploitation mechanics demonstrates that a remote attacker can execute arbitrary tag insertion by targeting any parameter placed within the dynamic ImageResponse layout structure. Consider a Next.js endpoint that reads the user's name from a query parameter and renders it inside a <title> tag.
An attacker supplies the following input vector via a GET parameter:
</title><rect x="50" y="250" width="500" height="150" fill="red"/><title>
During processing, Satori generates the output string by wrapping the parameter with <title> tags. Due to the lack of output escaping, the raw output contains nested, valid SVG structures:
<title></title><rect x="50" y="250" width="500" height="150" fill="red"/><title></title>
The downstream image-rendering library (typically a Rust-based or system-level library such as resvg) parses the generated string. It treats the injected <rect> as a root-level element, which is rasterized and rendered into the final PNG binary.
Automated scanning and verification of this vulnerability cannot rely on simple HTTP status codes or response size analysis. This is because both benign and malicious inputs return 200 OK statuses and valid PNG streams. Safe detection requires deep rendering side-channel analysis. This is achieved by deflating the PNG IDAT compression chunks, applying the inverse PNG filter, and programmatically counting the presence of injected colored pixels (such as the #FF0000 red pixels in the injected rectangle) at the target coordinates.
The direct result of CVE-2026-94545 is arbitrary markup injection within vector image files. While the base CVSS score for Satori as an isolated markup generator is calculated as 5.3, the actual operational impact within a Next.js web application is Critical. The severity is determined by how the downstream server processes the generated SVG markup.
First, many modern serverless platforms convert SVG structures into PNG formats using headless instances of Chromium or other browser engines. Under these conditions, an injected element containing <iframe src="http://169.254.169.254/latest/meta-data/"/> or <image href="http://169.254.169.254/"/> can trigger Server-Side Request Forgery (SSRF). This allows attackers to access internal network endpoints, container orchestration metadata services, or local resources.
Second, if the downstream rasterizer supports inline scripting or XML External Entity (XXE) resolution, attackers can use the injection primitive to execute local file reads. They can exfiltrate sensitive files, such as environment variables, AWS session tokens, or local credentials.
Finally, if the underlying SVG compilation backend is linked to an unpatched system library (such as older versions of librsvg or ImageMagick), memory corruption or arbitrary command execution vulnerabilities can be triggered directly on the server host.
Remediation of CVE-2026-94545 requires upgrading both the Next.js framework and any direct installations of the Satori package. Upgrading to Next.js 16.3.6 (or the backported security release 15.5.26) ensures the compiled @vercel/og bundle is updated to a secure version containing the structured XML serializer.
If immediate dependency updates are restricted by deployment freezes or legacy code constraints, developers must implement an input validation middleware. This middleware must sanitize query parameters before they reach the JSX structure. Using a regex-based substitution pattern to strip or encode critical XML control characters prevents tag escape sequences from being processed by Satori.
function sanitizeForSVG(input: string): string {
return input.replace(/[&<>"']/g, (char) => {
switch (char) {
case '&': return '&';
case '<': return '<';
case '>': return '>';
case '"': return '"';
case "'": return ''';
default: return char;
}
});
}Additionally, applications can migrate their dynamic Open Graph routes to run exclusively within the Edge Runtime instead of the Node.js environment. The Edge Runtime implementation of ImageResponse does not leverage the vulnerable downstream compilation paths, mitigating the exposure vector. Deploying restrictive Web Application Firewall (WAF) rules that detect nested XML tags inside image-related query parameters can provide supplementary edge detection.
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Next.js Vercel | >= 16.2.0, < 16.3.6 | 16.3.6 |
Satori Vercel | >= 0.0.27, < 0.33.5 | 0.33.5 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-116 (Improper Encoding or Escaping of Output) |
| Attack Vector | Network (AV:N) / Unauthenticated Remote HTTP Request |
| CVSS v4.0 Score | 9.8 (Critical Framework-Level Impact) |
| EPSS Score | Not Available |
| Exploit Status | Proof-of-Concept (PoC) documented and verified via rendering side-channel |
| CISA KEV Status | No (Not currently listed in the CISA KEV catalog) |
| Mitigation Strategy | Update Next.js or enforce strict input escaping and Edge runtime configurations |
Satori does not properly sanitize or escape input values before incorporating them into the generated XML-based SVG output. This permits a structural breakdown of the document boundaries, leading to arbitrary XML/SVG injection.
A security vulnerability in serialize-javascript v7.1.1 allows Cross-Site Scripting (XSS) due to an overly greedy regular expression (SCRIPT_CLOSE_REGEXP) used during function serialization. Two secondary defects involving a spoofed toString() validation bypass and a stateful native code validator are also addressed in the fixed version v7.1.2.
A Denial of Service (DoS) vulnerability exists in the PyJWT library when parsing unverified token payloads containing deeply nested JSON structures. Because PyJWT fails to catch RecursionError during payload parsing, an unauthenticated remote attacker can crash the application thread or worker by sending a specially crafted token.
An uncontrolled recursion vulnerability exists in PyJWT from version 2.13.0 to 2.14.0. The vulnerability allows remote, unauthenticated attackers to cause a Denial of Service (DoS) via crafted JWT headers that trigger stack exhaustion during JSON decoding.
A signature verification bypass vulnerability in PyJWT allows unauthenticated remote attackers to forge JSON Web Tokens when processing JSON Web Key Sets containing an empty symmetric key.
A Regular Expression Denial of Service (ReDoS) vulnerability exists in Nodemailer's addressparser fallback engine before version 10.0.6. Under specific malformed inputs with excessive word boundaries, the parser exhibits quadratic backtracking, leading to high CPU utilization and event loop blockage.
Nodemailer versions prior to 10.0.9 are vulnerable to a parser differential bug. When processing a quoted local-part followed by an RFC 5322 comment and trailing characters, the internal addressparser module fails to order its normalization routine correctly. This error results in the generation of malformed envelope recipient addresses containing injected whitespace and secondary domains, allowing attackers to bypass routing restrictions and exfiltrate sensitive emails.