Next.js OG image RCE (CVE-2026-94545) — why the same vulnerability scores 9.5 and 5.3
CVE-2026-94545 in Next.js next/og scores 9.5 in one advisory and 5.3 in another. What decides it is what processes the generated SVG.
On this page
On September 22, 2026, a remote code execution (RCE) vulnerability was disclosed in ImageResponse from next/og, Next.js's API for generating OG (Open Graph) preview images. Remote code execution means an attacker can run their own code on a server from across the network. There is a single identifier, CVE-2026-94545, yet the Next.js advisory rates it Critical 9.5, while the advisory for Satori, the SVG-generating library where the bug originates, rates it Moderate 5.3. The Satori advisory explains the gap in one sentence: "The impact depends on how the generated SVG is consumed." Based on the two advisories, the official blog post, and our own comparison of the Next.js 16.3.5 and 16.3.6 packages downloaded from npm, this article sorts out what is affected and what is not.
1. Executive summary
ImageResponse on the Node.js runtime. In the published code, the affected Node.js path and the unaffected Edge path differ by a single function argument in the SVG-to-PNG step: whether sharp, an image-processing package, is passed in. And because the official documentation marks runtime = 'edge' in route config as deprecated, we cannot recommend “move it to Edge” as a workaround.Bracketed numbers refer to the sources at the end. All figures were checked against the original sources on 2026-09-23.
2. What was disclosed
An OG image is the 1200×630 picture that appears as a preview when a link is shared in a messaging app or on social media. In Next.js, the basic example in the official documentation draws the image with JSX in an app/.../opengraph-image.tsx file and returns a PNG built with ImageResponse from next/og. Internally, ImageResponse first turns the JSX into SVG (a format that describes vector graphics in XML), then converts that SVG to PNG. The library that turns JSX into SVG is Satori. [5]
Four packages were published one after another on September 22. The times below are the publish times recorded in the npm registry (UTC). [7]
| Published (UTC) | Package | What it is |
|---|---|---|
| 2026-09-22 16:01 | satori 0.33.5 | Fixed Satori release. The affected range is 0.0.27 through 0.33.4 [2] |
| 2026-09-22 16:19 | next 16.3.6 | Fixed Next.js 16 release (Active LTS) |
| 2026-09-22 16:19 | @vercel/og 1.0.3 | OG package for use outside Next.js. Depends on satori 0.33.5 |
| 2026-09-22 17:03 | next 15.5.26 | Next.js 15 release (Maintenance LTS). Hardening only |
The official Next.js blog describes 15.5.26 this way: “Version 15.5.26 includes related hardening, but Next.js 15.x is not affected by the remote code execution issue.” The comment on the install command also reads npm install next@15.5.26 # for 15.5 (hardening only). This does not mean 15.x users have no reason to upgrade, but it does not mean 15.x can be compromised through this vulnerability either. [3]
Both advisories credit the same two people (RaghavMaheshwari124, rafabd1). Neither advisory mentions exploitation observed in the wild or public exploit code. [1] [2]
3. One CVE, two scores
CVSS is the common standard for scoring a vulnerability's severity from 0 to 10, and both advisories used the latest version, 4.0. The score is calculated from a combination of metrics called a vector; put the two advisories' vectors side by side and you can see which metrics differ.
| Metric | Meaning | Next.js advisory | Satori advisory |
|---|---|---|---|
| AV · AC | Attack vector · complexity | Network · Low | Network · Low |
| AT | Attack requirements | Present (P) | Present (P) |
| PR | Privileges required | None | None |
| UI | User interaction | None | Passive interaction required (P) |
| VC · VI · VA | Confidentiality · integrity · availability of the vulnerable system itself | High · High · High | None · None · None |
| SC · SI · SA | Confidentiality · integrity · availability of subsequent systems | High · High · High | High · High · None |
| Score | 9.5 Critical | 5.3 Moderate | |
What differs is VC·VI·VA, the damage to the vulnerable system itself. From Satori's point of view, the library's job ends at producing an SVG string. Even if, because escaping is missing, an attacker-supplied value is interpreted as SVG markup, Satori itself does not execute code. So the advisory scores damage only for “subsequent systems” (SC·SI), the ones that process the SVG afterward, and it says: “The impact depends on how the generated SVG is consumed.” Escaping means rewriting special characters so they are read literally. [2]
From Next.js's point of view, the components that receive and process that SVG are also within the product's scope. Next.js writes that “improper escaping in SVG output generated by Satori could lead to remote code execution due to vulnerabilities in other upstream dependencies,” and scores the damage to the server itself High across the board. [3]
4. Where does the SVG go?
So what are the “other upstream dependencies” that receive the SVG in Next.js? Neither the two advisories nor the official blog names them. So we downloaded the next 16.3.5 and 16.3.6 packages published on npm and compared the next/og code inside them (dist/compiled/@vercel/og/) ourselves. [7]
The Edge bundle and the Node.js bundle share the same function for converting SVG to PNG. Condensed to the essentials:
async function render(satori, resvg, sharp, opts, defaultFonts, element) {
const svg = await satori(element, { ... }) // JSX → SVG string
if (sharp) {
pngBuffer = await sharp(new TextEncoder().encode(svg))
.resize(options.width).png().toBuffer() // SVG → sharp
} else {
const resvgJS = new resvg.Resvg(svg, { ... }) // SVG → resvg.wasm
...
}
}
// call in the Edge bundle
await render(satori, resvg_wasm_exports, void 0, options, fonts, element)
// call in the Node.js bundle
await render(satori, resvg_wasm_exports, await getSharp(), options, fonts, element)
Edge passes void 0 (that is, undefined) as the third argument, so it always converts with resvg.wasm. The Node.js bundle passes the result of getSharp(), a function that tries import("sharp") and returns sharp if it is installed, or undefined if not. In other words, on the Node.js runtime, if sharp is installed, the SVG Satori generates goes into sharp. resvg.wasm is an SVG-to-PNG renderer built as WebAssembly, and sharp is an image-processing package that uses the native library libvips.
Between 16.3.5 and 16.3.6, this render() function did not change by a single character. What changed is on the Satori side. Two pieces of code are new in 16.3.6:
- Validation of XML element and attribute names — if a name does not follow XML rules, it throws an
Invalid XML ${e} nameerror. This string appeared 0 times in 16.3.5 - Attribute value escaping — attribute values are wrapped with
escape-html
The commit that brought in this fix is titled "[active-lts] Harden next/og SVG serialization" (September 22). So the fix did not change what receives the SVG; it stopped values an attacker supplies from becoming SVG markup in the first place.
libheif library used by sharp which Next.js uses for image optimization” — and its CVSS vector is identical to this one, character for character, at 9.5. [4]5. Conditions for being affected
AT:P in the CVSS vector means the attack has preconditions. Taken together, the Next.js advisory and blog describe four conditions, and all of them must hold. [1] [3]
| Condition | What to look at | Source |
|---|---|---|
| Version | next at 16.2.0 or later and below 16.3.6 | Next.js advisory |
| API | Uses ImageResponse from next/og (opengraph-image.tsx, twitter-image.tsx, or new ImageResponse(...) directly) | Next.js advisory |
| Runtime | Runs on the Node.js runtime (Edge is not affected) | Next.js advisory and blog |
| Input | Attacker-controlled values go into SVG content, attributes or styles | Next.js advisory |
Conversely, the advisory explicitly names two cases that are not affected: apps that use the Edge ImageResponse, and apps that do not put attacker-controlled values into the SVG. The official blog adds Next.js 15.x to that list.
The default is the affected side
In route segment config, the default runtime is 'nodejs', and 'edge' is marked (deprecated) in the official documentation. An OG route that does not explicitly declare export const runtime = 'edge' runs on the Node.js runtime. Because the setting for the unaffected Edge runtime is deprecated, we cannot recommend switching to Edge as a workaround for this issue. Upgrading is the answer. [6]
“Attacker-controlled values” cover more than you might think
The official documentation says that, by default, generated images are created statically at build time and cached, and are rendered per request only when they use Request-time APIs (query strings, headers, cookies and so on) or uncached data. An OG image built once at build time from trusted values leaves no place for attacker input to reach. [5]
Things change, though, if the path has a dynamic segment such as [slug]. dynamicParams defaults to true, so an image for a value that was not generated at build time is rendered on the spot when a request for it arrives. According to the official documentation, OG routes are “specialized Route Handlers that can use the same route segment configuration options as Pages and Layouts.” So if your code receives a request for /posts/any-string/opengraph-image and draws that string straight into JSX, it renders with that value the first time it is requested, and a string the attacker chose goes into the SVG. The image is cached once it has been generated, but the risk depends on whether an attacker-chosen value actually goes into the JSX at the moment of rendering. The documentation's own main example also looks up a post using a value received through params and puts its title into the JSX. If that title comes from a post a user wrote, it too is an attacker-controlled value. [5] [6]
6. Satori outside Next.js
Satori is not specific to Next.js. It is also used in other frameworks' OG image generators and in home-built image pipelines. In those cases, upgrading Next.js changes nothing.
| How you use it | Upgrade to | What we checked |
|---|---|---|
satori installed directly | 0.33.5 or later | Fixed version in the Satori advisory [2] |
@vercel/og installed directly | 1.0.3 or later | On npm, 1.0.3 depends on satori 0.33.5; 1.0.2 depends on satori 0.33.3 [7] |
next/og (inside Next.js) | next 16.3.6 | Next.js advisory [1] |
Where security scanners may get confused
While comparing the published packages, we checked one more thing. The OG code inside next 16.3.6 is still labeled @vercel/og 0.11.1 and satori@0.25.0, the same labels as in 16.3.5. The official blog says the releases “upgrade upstream dependencies, including Satori,” but inside the bundle the label was not raised to 0.33.5; what is there is 0.25.0 with the fix added on top. [3] [7]
In practice this can play out in two ways. The first is a possible false-positive scenario — a tool that decides whether something is vulnerable from the version labels inside a package could read 16.3.6 as “satori 0.25.0, below 0.33.5” and flag it as still vulnerable. We have not observed a real scanner flagging it this way. If you see such a warning, do not decide from the label alone; look at the next version. Second, you can also go wrong in the opposite direction — Next.js being fixed does not mean that a satori or @vercel/og installed separately in the same repository is fixed too. Why bundled dependencies need to be managed separately is also covered in Supply chain security for AI coding tools.
rsvg-convert, and deploy it as a static file. There is no request-time rendering and no user input, so this issue's conditions do not apply.7. How to check your project
Go through the four conditions above, cheapest to check first. If an earlier step rules you out, you can stop there.
① The version actually installed
Look at the version pinned in the lockfile, not at a range like ^16.3.4 in package.json.
npm ls next # or: pnpm why next / yarn why next
node -p "require('next/package.json').version"
② Where next/og is used
grep -rn --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \
-E "from ['\"]next/og['\"]|ImageResponse" app src pages 2>/dev/null
find app src -name 'opengraph-image.*' -o -name 'twitter-image.*' 2>/dev/null
If it is an image file, such as opengraph-image.png, it does not use ImageResponse, so it is not affected. Look only at files that draw the image in .tsx, .ts or .js.
③ Runtime and sharp
grep -rn "export const runtime" app src 2>/dev/null # if none, the default is nodejs
ls node_modules/sharp 2>/dev/null && echo "sharp installed"
sharp is the package Next.js uses for image optimization, so it may already be installed in projects that use next/image. [4]
④ What goes into the JSX
Open the files you found in ② and check whether any of the following values go into the JSX passed to ImageResponse.
- Dynamic segment values received through
params([slug],[id], etc.) - Values taken from
searchParamsor from request headers or cookies - Titles, names or descriptions written by users, fetched from a database or an external API
- Any of the above going into
styleor SVG attributes (colors, font names, image URLs, etc.)
If any of these applies and you also meet the version condition in ①, keeping those values out of the JSX until you upgrade is the only interim measure the advisory gives. The Satori advisory says: “No complete workaround exists besides upgrading.” [2]
8. Checklist
- If you are on Next.js 16.2.0 or later but below 16.3.6, upgrade to 16.3.6. Do this before working through the conditions. It is the cheapest measure
- On Next.js 15.x, this RCE does not apply. 15.5.26 is hardening, so include it in your next regular update
- If you installed
satoridirectly, upgrade it separately to 0.33.5 or later; if you installed@vercel/ogdirectly, to 1.0.3 or later - Until you upgrade, keep request values and user-written content out of the JSX in dynamic OG routes
- If a security scanner flags the
satori@0.25.0inside 16.3.6 as vulnerable, do not judge from the label alone; check thenextversion - Do not treat switching the runtime to Edge as the fix. The official documentation marks
runtime = 'edge'in route config as deprecated - Also check the August 25 AVIF image-optimization RCE (fixed in 16.3.3). Below 16.3.3, both issues may apply
9. What this is not yet
- The component that actually executes code has not been disclosed. The “sharp path” in Section 4 is our inference from the published code and the August advisory. The two advisories and the official blog refer only to other upstream dependencies, without naming them
- The advisory does not say whether Node.js environments without sharp installed are safe. In the code, such environments convert with resvg.wasm just as Edge does, but the advisory names “the Node.js
ImageResponseimplementation” as affected without distinguishing by whether sharp is present. Do not conclude you are safe on the strength of this difference; upgrade - We could not check the status of apps deployed on Vercel. Neither the official blog nor the advisories mention impact by hosting environment
- We could not confirm the details of the attack technique. The Satori advisory does not say which values were not escaped; it says only “certain values.” We confirmed only that the fixed code adds element and attribute name validation and attribute value escaping
- Whether it has been exploited is unknown. The advisories do not mention observed exploitation or public exploit code; that is not confirmation that there have been no attacks
- We did not reproduce the attack ourselves. What we did directly for this article is check the publish times in the npm registry and compare the 16.3.5 and 16.3.6 package code
10. Sources
| Source | What we checked | Scope of the evidence |
|---|---|---|
| [1] Next.js security advisory GHSA-vcvr-r3jv-pc5j | CVE-2026-94545, Critical 9.5, CVSS 4.0 vector, affected range 16.2.0 or later and below 16.3.6, conditions under which apps are not affected, reporters | Vendor's primary advisory · checked 2026-09-23 |
| [2] Satori security advisory GHSA-wx4j-mvgx-mqwp | Moderate 5.3 for the same CVE, vector, affected range 0.0.27 or later and below 0.33.5, “The impact depends on how the generated SVG is consumed.” | Vendor's primary advisory · checked 2026-09-23 |
| [3] Next.js Security Update for a Critical Upstream Issue | The 16.3.6 and 15.5.26 releases, 15.x not affected by the RCE, Edge not affected, “other upstream dependencies” | Vendor's official blog · checked 2026-09-23 |
| [4] Next.js security advisory GHSA-2xp9-vwfh-vxw4 | The August 25 AVIF RCE. Names libheif, used by sharp, as the cause; the same 9.5 vector; fixed in 16.3.3 and 15.5.24 | Vendor's primary advisory · checked 2026-09-23. A comparison case, not evidence that this issue has the same cause |
| [5] Next.js docs: opengraph-image and twitter-image | Static generation by default, dynamic rendering only with Request-time APIs, the main example that uses params, specialized Route Handlers | Official documentation (16.3.5 version) · checked 2026-09-23 |
| [6] Next.js docs: Route Segment Config | runtime default 'nodejs', 'edge' marked deprecated, dynamicParams default true | Official documentation (16.3.6 version) · checked 2026-09-23 |
[7] npm registry · published next 16.3.5 and 16.3.6 packages | Publish times of the four packages, the satori version @vercel/og 1.0.3 depends on, the render() code and satori@0.25.0 label inside the bundle, the code added in 16.3.6 | Downloaded and compared by us · 2026-09-23. The reading of the code is ours |
Editorial standard: we kept values from the advisories, blog and documentation separate from values we confirmed in the published packages. The “sharp path” in Section 4 is flagged as inference both in the body and in the limits section. We did not use figures from summary articles as evidence.