// threat intel

CVE-2026-94545: When Your OG Image Generator Becomes an RCE (Next.js ImageResponse)

On September 22, Vercel patched a critical flaw in Next.js that turns a feature almost nobody thinks of as security-sensitive into a path to server-side code execution. The feature is ImageResponse, the thing that generates your Open Graph and social preview images. The CVE is CVE-2026-94545, rated critical at CVSS 9.5, and if your app feeds any attacker-controlled text into those generated images, you are potentially exposed.

We flag this class of bug in web application assessments constantly: a “content” feature that quietly compiles user input into a richer format, in this case SVG, without escaping it first. It rarely shows up on a developer’s threat model because “we just draw a preview image” does not sound dangerous. That assumption is exactly what makes it dangerous.

What broke

Next.js ImageResponse does not draw pixels directly. It uses Satori, a Vercel library, to convert your image layout into SVG first, and only then renders that SVG to a PNG. The vulnerability lives in Satori: certain values reached its SVG output without being properly escaped. A carefully constructed value stops being treated as plain text and starts being parsed as SVG markup.

That is a textbook injection primitive. Once you can inject into the SVG, that markup flows onward through other libraries Next.js depends on during rendering, and Vercel says this is where it becomes code execution on the server. Vercel has not named which downstream libraries carry the executable sink, but the chain is clear enough: unescaped input, SVG parser, and something further down that treats part of that SVG as more than a picture.

The advisory’s own proof of concept takes a value straight from the request URL and drops it into an SVG title element. Notably, Vercel does not confirm whether text placed in ordinary elements, like a heading inside a div, is equally exploitable. From an assessment standpoint, treat that ambiguity as “assume yes until proven otherwise.” An unconfirmed safe path is not a safe path.

Who is actually affected

The conditions are specific, and getting them right saves you a panic:

  • Next.js 16.2.0 through 16.3.5, when ImageResponse runs on the Node.js runtime. Node is the default runtime, so most apps using the feature are on it.
  • The app must pass attacker-controlled values into SVG content, attributes, or styles during image generation. Text read from the request URL is the obvious example.
  • The Edge version of ImageResponse is not affected. Next.js 15 is not affected either.

Affected versions have been shipping since Next.js 16.2 landed on March 18, so this has been reachable for roughly six months on any app that upgraded early to the 16.2 line.

Finding it in your own codebase

This is the part worth doing carefully, because the standard tooling will lie to you right now. As of September 23, npm audit did not flag Next.js 16.3.5, an affected version. The advisory was not yet in the GitHub Advisory Database, and no CVE record for CVE-2026-94545 had been published. In other words, your CI dependency scan will happily tell you everything is fine while you are sitting on a 9.5.

So check by hand:

  • Check the Next.js version directly in your lockfile, not through an audit tool. If it is 16.2.0 through 16.3.5, you are in range.
  • Do not go looking for Satori as a dependency. It is bundled inside the Next.js package, so it will not appear as a listed dependency of Next.js in your lockfile. Grepping for satori will come up empty and give you false comfort.
  • Grep for the feature, not the library. Look for ImageResponse imported from next/og. The usual homes are route handlers and opengraph-image files. Route handlers build the image when a request arrives; an opengraph-image file can build it at build time or per request.
  • Trace the data flow. Finding ImageResponse is step one. The real question is whether any value inside it originates from the request, be it URL params, query strings, headers, or anything a client controls. That trace is exactly what a secure code review is built to catch, since it is the finding, not the import statement.

The fix, and the version trap

The patched release is Next.js 16.3.6, installed with npm install next@16.3.6. It is the only fixed version, and there is a trap in the versioning:

  • As of September 23, the npm registry listed no fixed release for the 16.2 line. If you are on 16.2, you cannot stay on it and patch. You must move to 16.3.6.
  • Next.js 15.5.26 adds extra hardening for next/og on the 15.5 line. Next.js 15 is not vulnerable to this CVE, but if you are on 15.5 it is worth picking up.
  • If you use Satori directly, outside of Next.js, update it to 0.33.5, which carries the fix. Worth noting: Satori’s own advisory rates the same CVE only moderate at 5.3, because in isolation the impact depends on how the SVG output is consumed. Inside Next.js, that same bug becomes a 9.5. Same root cause, wildly different severity depending on what sits downstream. That gap is a good reminder that a dependency’s self-rated CVSS is not your risk.

If you cannot upgrade immediately

The only workaround Vercel offers is to keep attacker-controlled values out of the SVG content, attributes, and styles that the Node.js ImageResponse renders. In practice that means auditing every ImageResponse call, most of which live behind an API route, and stripping or hard-coding any request-derived value until you patch. An API penetration test of those routes is a reasonable way to confirm the stripping actually holds before you call the interim fix done. Note what Vercel does not suggest: switching to the unaffected Edge runtime. That is deliberate, because the Next.js docs already mark the Edge runtime as deprecated, so migrating onto a deprecated runtime to dodge one CVE trades a known problem for a dead end.

Exploitation status and the visibility gap

As of September 23, there were no public reports of attacks using this flaw and no public exploit code. That is the calm before, not the all-clear. A CVSS 9.5 with a published advisory and a clear injection primitive is the kind of thing exploit developers reverse quickly once the patch diff is public, and the patch is now public.

Two visibility problems make this worse than the raw numbers suggest. First, as covered above, your dependency scanners are not yet catching it, so plenty of vulnerable apps will not even know. Second, Vercel provides no way to check whether an affected route was abused before the patch. There is no indicator to grep your logs for out of the box, which means detection falls back on you: review access to any route that hits an affected ImageResponse and look for anomalous or markup-laden values in the parameters that feed the image.

One open question if you host on Vercel: the advisory does not say whether apps hosted on Vercel are automatically protected. For two critical Next.js flaws patched back in August, Vercel stated hosted apps were protected and needed no upgrade. It has made no such statement this time. Until it does, do not assume the platform covers you, and if you are unsure what your own hosting configuration actually enforces, a cloud configuration review will tell you rather than a vendor blog post. Patch the app.

The takeaway

The interesting thing about CVE-2026-94545 is not the CVSS number, it is the shape. A feature built to render a harmless preview image became remote code execution because user input was compiled into SVG without escaping, and SVG turned out to be a gateway to executable sinks deeper in the stack. This is the pattern to internalize: any feature that transforms user-supplied text into a structured format, whether SVG, XML, a template, or a document, is an injection surface whether or not anyone labeled it one, the same class our OWASP Top 10 testing is built to hunt down before an attacker treats your preview image as a shell. If you run Next.js with ImageResponse, check your version by hand, trace your inputs, and move to 16.3.6.

// get started

Work with CyprusPentest

Tell us about your target and goals. We’ll reply with scope and a fixed-price quote — usually within one business day.

./request_engagement