The Same Bug Was Moderate in Satori and Critical in Next.js

The Same Bug Was Moderate in Satori and Critical in Next.js

Vercel shipped an out-of-band security release on September 22, 2026, and the same defect was published twice that day with two different severities. The Satori advisory, GHSA-wx4j-mvgx-mqwp, rates it Moderate, CVSS 4.0 base 5.3. The Next.js advisory, GHSA-vcvr-r3jv-pc5j, rates it Critical, CVSS 4.0 base 9.5. Both carry the identifier CVE-2026-94545. Both were published by the same GitHub account, KarimPwnz, on the same afternoon.

One bug. Two scores. The gap is not a disagreement, and it is not a scoring mistake. It is the point.

What actually broke

ImageResponse from next/og generates images from JSX and CSS. It hands that markup to Satori, a Vercel library that converts it to SVG, and then rasterizes the SVG into a PNG. The route is the standard shape of a social card: dynamic, unauthenticated by design because crawlers have to reach it, and driven by whatever the request carries. The advisory’s own example is four lines:

import { ImageResponse } from 'next/og'

export async function GET(request: Request) {
  const value = new URL(request.url).searchParams.get('value') ?? ''

  return new ImageResponse(
    <svg width="1200" height="630">
      <title>{value}</title>
    </svg>
  )
}

Satori’s advisory states the failure plainly: it “does not properly escape certain values before including them in generated SVG output. This can allow crafted values to be interpreted as SVG markup.” A value that was supposed to be text in a title element arrives in the generated SVG without escaping, closes its context, and becomes markup instead. The advisory then adds the sentence that determines everything downstream of it: “The impact depends on how the generated SVG is consumed.”

Next.js’s advisory completes the chain without naming the middle link. The Node.js ImageResponse implementation “is affected by an upstream vulnerability. This can lead to remote code execution,” and the condition is precise: “Affected applications pass attacker-controlled values into SVG content, attributes, or styles during image generation.” The fix upgrades upstream dependencies, and Vercel attributes the last step to “vulnerabilities in other upstream dependencies.” Those downstream libraries are not named in either advisory, so the exact final conversion from injected markup to executed code is unreported; what is confirmed is the direction of the chain and the deployment condition that opens it.

The escalation path, as documented:

request URL or stored user input
  -> JSX passed to ImageResponse
    -> Satori serializes it to SVG without escaping some values
      -> crafted value becomes SVG markup instead of text
        -> Node.js runtime consumes the generated SVG
          -> remote code execution on the application server

Read the two vectors and the argument writes itself

Satori (GHSA-wx4j-mvgx-mqwp)Next.js (GHSA-vcvr-r3jv-pc5j)
SeverityModerateCritical
CVSS 4.0 base5.39.5
VectorAV:N/AC:L/AT:P/PR:N/UI:P/VC:N/VI:N/VA:N/SC:H/SI:H/SA:NAV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Affected>= 0.0.27 < 0.33.5>= 16.2.0 < 16.3.6
Patched0.33.516.3.6

Compare the exploitability half first. Attack vector network, complexity low, privileges none, attack requirements present, in both. That last metric is CVSS 4.0’s way of saying the deployment itself is a prerequisite: an application has to feed untrusted values into the SVG. The only difference in that half is user interaction, passive versus none, worth a fraction of a point.

Now compare the impact half. Satori scores VC:N VI:N VA:N. No impact on the vulnerable system at all. A library whose job is to return a string of SVG is not harmed by returning a slightly wrong string. Its subsequent system impact is SC:H SI:H: confidentiality and integrity high, but on something else. Next.js scores all six impact metrics high, on the framework and on what follows it, because in that path the same string is not returned to a caller. It is consumed by a runtime that can reach the host.

The 4.2 point difference is entirely the identity of the consumer. Nothing about the escaping bug changed between the two advisories. What changed is what sits on the other side of it.

This is the part worth carrying out of the incident: severity is a property of the pipeline, not of the bug. The same CWE, the same commit, the same CVE number, and the two published numbers differ by a factor that makes one a routine dependency bump and the other an emergency. A security program that triages by the library’s own advisory score will rank this defect as housekeeping in the repository where it is critical.

The workaround is not input validation

The instinct on a dynamic image route is to sanitize: strip angle brackets, escape quotes, cap the length of the title parameter. That closes the wrong layer.

The Satori advisory is explicit about it: “No complete workaround exists besides upgrading. Applications that cannot immediately upgrade should not render attacker-controlled content with Satori.” Note the wording. Not “filter the content”, not “validate the value”. Stop rendering it. The defect is in serialization, in the step where a value is written into a structure, so the content of the value is not the right place to look for a fix. Any filter is an attempt to guess, at the input boundary, every string that could escape a context you do not control downstream.

There is also a supply chain trap in the remediation. Upgrading Next.js to 16.3.6 upgrades the Satori that Next.js bundles, not the Satori you installed yourself. The two fix releases landed 20 minutes apart on September 22, and they are separate installs:

  • satori 0.33.5 published 2026-09-22T16:01:32Z
  • @vercel/og 1.0.3 published 2026-09-22T16:19:35Z
  • next v16.3.6 published 2026-09-22T17:15:10Z

Versions confirmed against the npm registry and the Next.js release tag. If your lockfile resolves Satori for direct use, the framework bump will not move it.

What to do, in order

Inventory the routes. Search the codebase for next/og, ImageResponse, opengraph-image, and twitter-image. Runtime matters more than presence: App Router routes run on the Node.js runtime by default, and only a route explicitly set to Edge is out of scope. Vercel’s advisory lists affected versions as >=16.2.0 <16.3.6, and the Edge implementation of ImageResponse as unaffected.

Trace the data, not the code. Ask which visitor-influenced values reach SVG content, attributes, or styles. Query parameters, post titles, usernames, product names, campaign slugs. The uncomfortable case is user-generated content stored in your own database: it feels internal, and it is still an attacker-controlled value by the time it reaches the renderer.

Upgrade to 16.3.6 and expect no backport. The advisories list 16.3.6 as the patched version, so an application on the 16.2 line moves forward rather than sideways. Next.js 15.5.26 shipped in the same out-of-band update as hardening, and per Vercel, Next.js 15.x is not affected by the remote code execution issue.

Fix direct dependencies yourself. Update satori to 0.33.5 or later and check @vercel/og separately. Reinstall so the lockfile moves, then re-read the diff.

Until the upgrade deploys, remove the input. Either stop passing untrusted values into SVG content, attributes, and styles, or generate the card without request-time input at all. Static generation at build time deletes the attack surface instead of bounding it. This site’s own social cards are cropped from each post’s hero image at publish time, which is why the pattern described in the advisory never applied here.

Assume the route was reachable. If all three conditions held and you ran an affected version, the image endpoint was publicly reachable by design. Review what that server process could read: environment variables, cloud credentials, internal service addresses, database clients. The advisories do not state whether the issue has been exploited, and as of this writing NVD still returns zero records for CVE-2026-94545, so there is no NVD-side confirmation to check. Treat malformed or unusually long parameters against image routes as worth an alert, and treat credential rotation as the cheap end of the response.

Why this generalizes past Next.js

Agent systems are full of the same shape. An agent reads untrusted text and emits structure: SVG, HTML, JSON, a shell command, a SQL string, a YAML config. Every one of those serialization steps is a place where data can become structure, and every one of them sits in front of a different consumer. The same escaped-string bug is a cosmetic glitch when the output is returned to a caller, a stored XSS when the output is a web page, and code execution when the output is fed to something that resolves external references or runs embedded content.

That asymmetry has an operational consequence for how agent platforms get built. The library is not the unit of risk. The pipeline is. If you are choosing where to spend your hardening effort, the useful question is not “does this dependency have CVEs”, it is “what consumes what this thing returns, and what authority does that consumer hold”. A renderer that ends in a browser is a different security domain from a renderer that ends in a process with your cloud credentials.

CVE-2026-94545 will be filed in two places with two numbers, and both are correct. Moderate in Satori, because Satori only produced a string. Critical in Next.js, because something else turned it into execution. Patch the dependency, then go read what consumes it.

Sources

Keep reading