GHSA-2464-fhr4-w9xw (CVE-2026-96889): Unauthenticated RCE in Next.js Image Optimization
Vercel rates GHSA-2464-fhr4-w9xw Critical, CVSS v4.0 9.5: an unauthenticated attacker can run code on a Next.js 10.0.0–15.5.27 or 16.0.0–16.4.0 server whose Image Optimization API processes SVG on glibc-based Linux. The bug is a use-after-free in librsvg (CVE-2026-96889), bundled in sharp's prebuilt binaries. Next.js 15.5.28 and 16.4.1 fix it on Node.js 20.9.0 or later by resolving sharp 0.35.5, which bundles the patched librsvg 2.63.2.
TL;DR
Vercel rates GHSA-2464-fhr4-w9xw Critical (CVSS v4.0 9.5): an unauthenticated attacker can run code on a Next.js server that optimizes SVG on glibc-based Linux. Upgrade to Next.js 16.4.1 or 15.5.28.
- The built-in Next.js Image Optimization API (
/_next/image) passes attacker-supplied image bytes tosharp, whose prebuilt libvips binaries embedlibrsvg, the standard SVG renderer. Affected librsvg builds are every release below the patched set: main-branch releases before 2.63.2 and stable-series releases before the 2.62.4, 2.61.5, 2.60.3 and 2.57.5 backports, per RUSTSEC-2026-0305. These versions free an XML entity while libxml2 still uses it: a use-after-free, published as CVE-2026-96889. - Every Next.js release from 10.0.0 through 15.5.27 and from 16.0.0 through 16.4.0 is affected when it runs on a glibc-based Linux system and SVG files can reach the optimizer. Vercel confirms a public proof of concept exists for the librsvg bug.
- No exploitation in the wild is known as of October 23, 2026, and the issue is not in the CISA Known Exploited Vulnerabilities catalog.
- Upgrade to Next.js 16.4.1, or 15.5.28 on the 15.x line. On Node.js 20.9.0 or later, a fresh install resolves sharp 0.35.5 (librsvg 2.63.2). On 15.x, run
npm ls sharpafter upgrading and installsharp@0.35.5if a 0.34.x build remains. Vercel's advisory names one workaround: ensuresharpresolves to 0.35.5 or later.
Am I affected?
npm recorded 44,586,009 downloads of next in the week of October 16-22, 2026. Run the two-minute check below.
You are affected if all of these hold:
- Next.js runs in an affected range.
- The server runs on a glibc-based Linux system, the common case for Node deployments.
sharpresolves to a build whose bundled librsvg is unpatched. The sharp and librsvg table below maps each build.- SVG content can reach the Image Optimization API.
| Branch | Affected versions | Fixed version |
|---|---|---|
| Next.js 16.x | 16.0.0 – 16.4.0 | 16.4.1 |
| Next.js 10.x – 15.x | 10.0.0 – 15.5.27 | 15.5.28 |
Starting with Next.js 15.0.0, next lists sharp under optionalDependencies, so it installs by default. Next.js 12.3.0, 13.5.6 and 14.2.32 declare no sharp dependency. On those lines the optimizer loads sharp only if your project installs it, and falls back to Squoosh otherwise.
The resolved sharp version decides which librsvg build your server embeds:
| sharp | Bundled librsvg (sharp-libvips) | Status |
|---|---|---|
| 0.33.5 | 2.58.93 (via @img/sharp-libvips-* 1.0.4) | Unpatched; outside every fixed range in the RustSec matrix |
| 0.34.3 | 2.60.0 (via @img/sharp-libvips-* 1.2.0) | Unpatched; the 2.60-series backport fix is 2.60.3 |
| 0.34.4 | 2.61.1 (via @img/sharp-libvips-* 1.2.3) | Unpatched; the 2.61-series backport fix is 2.61.5 |
| 0.34.5 | 2.61.2 (via @img/sharp-libvips-* 1.2.4) | Unpatched; the 2.61-series backport fix is 2.61.5 |
| 0.35.4 | 2.62.91 (via @img/sharp-libvips-* 1.3.3) | Unpatched; 2.62.91 predates the 2.63.2 main-branch fix and the 2.62.4 backport |
| 0.35.5 | 2.63.2 (via @img/sharp-libvips-* 1.3.4) | Contains the fix |
Check what is installed:
# Next.js and sharp versions
npm ls next sharp
# Direct read of the installed versions
node -e "console.log('next', require('next/package.json').version)"
node -e "console.log('sharp', require('sharp/package.json').version)"
# Lock file check (lockfileVersion 2 and 3)
grep -n '"node_modules/next"' package-lock.json | head
grep -n '"node_modules/sharp"' package-lock.json | head
# lockfileVersion 1 keys packages by bare name
grep -n '"next": {' package-lock.json | head
grep -n '"sharp": {' package-lock.json | head
# Container image check
docker run --rm <your-image> node -e "console.log(require('next/package.json').version, require('sharp/package.json').version)"
Sharp 0.35.x declares engines.node: >=20.9.0, so Node 18 is outside its supported runtime range. That field does not make npm choose an older compatible sharp: unless engine-strict is enabled, npm treats it as advisory and warns about an incompatible runtime.
- Next.js 15.0.0's
^0.33.5dependency range permits sharp 0.33.5, which bundles librsvg 2.58.93. - Next.js 15.5.28's
^0.34.3 || ^0.35.5range permits both the unpatched 0.34.x line and patched 0.35.5; an existing lockfile can retain 0.34.x.
The 0.33.x and 0.34.x builds in the sharp/librsvg table contain no patched librsvg.
How do I fix it?
Upgrade Next.js. On Node.js 20.9.0 or later, a fresh resolution pulls sharp 0.35.5 (librsvg 2.63.2). Check two exceptions:
- On the 15.x line the range still accepts
^0.34.3, so an existing lockfile can keep an unpatched 0.34.x build. Runnpm ls sharpafter the upgrade and installsharp@0.35.5explicitly if it remains on 0.34.x. - Node 18 hosts on the 15.x line must move to Node.js 20.9.0 or later before the fix is live.
# Next.js 16 line
npm install next@16.4.1
# or: pnpm add next@16.4.1 | yarn add next@16.4.1
# Next.js 15 line
npm install next@15.5.28
If you cannot move Next.js yet, force the sharp upgrade alone. The advisory's stated workaround is to ensure the version of sharp used by Next.js is 0.35.5 or greater. This requires Node.js 20.9.0 or later, because 0.35.x is the only sharp line that bundles a patched librsvg.
npm install sharp@0.35.5
That install only changes what Next.js loads when its own sharp range accepts 0.35.5. Next.js 15.5.27 declares ^0.34.3 || ^0.35.4, which does. Next.js 15.0.0 declares ^0.33.5, which does not, so its nested 0.33.5 stays in place. In that case, force the resolution in package.json and reinstall:
{
"overrides": { "sharp": "0.35.5" }
}
Use pnpm.overrides with pnpm and resolutions with Yarn.
Limits of the workaround: it only replaces the bundled librsvg. It does not change how the optimizer handles requests.
- The 16.4.1 fix requires
sharp ^0.35.5, which delivers the patched librsvg 2.63.2. - The 15.5.28 fix keeps the
^0.34.3floor in its^0.34.3 || ^0.35.5range. That floor carries no fix: the 0.34.x builds bundle librsvg 2.60.0 to 2.61.2, all below the 2.60.3 and 2.61.5 backports. - On Node 18, the 15.x upgrade is incomplete. Move to Node.js 20.9.0 or later to run sharp 0.35.5 on a supported runtime, then verify the resolved sharp version.
What is the root cause?
A use-after-free occurs when code frees memory and keeps using the pointer to it. In librsvg, the Rust library that renders SVG files by parsing their XML with libxml2, a nested XInclude with duplicate entity declarations frees an xmlEntity struct the outer parse still holds. XInclude is a mechanism that lets one XML document pull in nodes from another. Red Hat's CVE-2026-96889 record describes this sequence.
RUSTSEC-2026-0305 describes the failure in four steps:
- During expansion of an internal entity, libxml2 recursively parses an XInclude document that redeclares the same entity name.
- The two parses share librsvg's
XmlStateentity map. Adding the duplicate displaces the original entry. - Dropping the displaced
xmlEntityinvokesxmlFreeNode(), releasing its memory. - The outer
xmlCtxtParseEntity()continues to access that freed, 144-byte structure.
In one sentence: the entity map could replace an entry without checking whether libxml2 still held a pointer to it, so a document that re-declares an entity name across an XInclude boundary turns a parser-internal pointer into a dangling one.
That is the missing lifetime check, and it is what the upstream fix removes. Librsvg commit 8a1b0cd319e9af2d1e9cf878081dd77f227a0504 stops freeing xmlEntity pointers that libxml2 is still using. The fix first shipped in librsvg 2.63.2, with backports to 2.62.4, 2.61.5, 2.60.3 and 2.57.5, and it was made public on September 23, 2026.
Next.js reaches this code through image optimization. The /_next/image endpoint accepts an url parameter, checks it against the configured allowlists (images.domains on older lines, images.localPatterns and images.remotePatterns on newer lines), fetches the bytes, and passes them to sharp, which loads the image through libvips. Next.js 16.4.0's optimizer explicitly enables libvips's SVG loader for this, in packages/next/src/server/image-optimizer/transform.ts:
// packages/next/src/server/image-optimizer/transform.ts (v16.4.0)
export function getSharp(concurrency, operationCache) {
...
_sharp.block({ operation: ['VipsForeignLoad'] })
_sharp.unblock({
operation: [
'VipsForeignLoadHeif', // avif
'VipsForeignLoadJpeg',
'VipsForeignLoadNsgif',
'VipsForeignLoadPng',
'VipsForeignLoadSvg', // librsvg parses SVG bytes here
...
],
})
When libvips identifies the input as an SVG, VipsForeignLoadSvg hands the document to librsvg, and the entity-map bug is reachable from the request bytes. The same VipsForeignLoadSvg unlock exists in the 15.5.x optimizer. In older affected releases the plumbing differs: Next.js 14.2.x, for example, passes any buffer it cannot identify by magic bytes straight to sharp for format detection, in packages/next/src/server/image-optimizer.ts (v14.2.32):
// Fallback to sharp if available
if (sharp) {
const meta = await sharp(buffer)
.metadata()
.catch((_) => null)
switch (meta?.format) {
...
case 'svg':
return SVG
Note the order in that version: sharp(buffer).metadata() parses the file before the optimizer's dangerouslyAllowSVG check runs, so on that code path the SVG gate does not act as a boundary against parsing, only against returning the image.
How is it exploited?
There is no public end-to-end exploit against Next.js. For the librsvg bug itself a public reproducer exists: the GNOME GitLab tracker item for the issue is tagged as containing exploit material, and CISA's vulnrichment record for CVE-2026-96889 marks exploitation as "poc" with technical impact "total". We do not reproduce it here; the steps below describe the published path without exploit code.
- The attacker crafts an SVG that contains an XInclude of a second document, where the second document declares an XML entity with the same name as one already declared in the outer document.
- The attacker places that SVG where the target's optimizer can fetch it: a local path or remote host that passes the app's
imagesallowlist. - The attacker requests it through the image optimization endpoint, with a width in the app's configured
deviceSizesorimageSizesand a quality in range, for exampleGET /_next/image?url=https%3A%2F%2Fcdn.attacker.example%2Favatar.svg&w=640&q=75. - The optimizer fetches the bytes and passes them to sharp. Libvips identifies the document as an SVG and librsvg parses it.
- During entity expansion, the nested XInclude re-declares the entity. The map replaces the old entry and frees it, while the outer parse keeps a pointer to it.
- The outer parser reads the freed 144-byte
xmlEntity: a use-after-free. On a glibc heap, an attacker who controls the freed content and the allocation sequence can turn this into arbitrary code execution inside the Node.js server process.
The advisory scopes the RCE to glibc-based Linux because the prebuilt sharp binaries that Next.js installs are glibc builds; the musl builds are separate packages.
How does the exploit flow through the optimizer?
nested XInclude and duplicate entity"] --> B["GET /_next/image?url=...&w=...&q=..."] B --> C["URL allowlist check
images config"] C --> D["Optimizer passes bytes to sharp
libvips selects VipsForeignLoadSvg"] D --> E["librsvg entity map
replaces and frees in-use entry"] E --> F["Outer xmlCtxtParseEntity reads
freed 144-byte xmlEntity"] F --> G["Use-after-free leads to
code execution in server process"]
The missing check is node E in the diagram: nothing verifies the entity is out of use before it is freed.
What can an attacker do?
Successful exploitation runs arbitrary code with the privileges of the Next.js server process, typically the Node.js process serving the application. That means access to everything the process can reach: environment variables and deployment credentials, internal services, databases, and the ability to modify responses served to users. Failed or partial exploitation can also crash the process, because the dangling pointer is dereferenced whether or not the attacker wins the allocator race.
The conditions an attacker needs:
- No authentication and no user interaction, per the CVSS v4.0 vector
AV:N/AC:L/AT:P/PR:N/UI:N(network, low complexity, no privileges, no user interaction; attack requirements present). - The server runs on a glibc-based Linux system.
sharpresolves to a build whose bundled librsvg is unpatched (0.35.4 bundles librsvg 2.62.91; 0.34.x bundles 2.60.0, 2.61.1 or 2.61.2; 0.33.5 bundles 2.58.93).- SVG bytes can reach the optimizer: the attacker needs a source URL that passes the app's
imagespattern allowlist. On Next.js 14.2.x, the SVG parse happens before thedangerouslyAllowSVGcheck, so that flag gates only the response, not the vulnerable parsing. - A width and quality combination the app accepts.
Why does the fix work?
Vercel fixed this by bumping the bundled librsvg via the sharp dependency, not by changing request handling. The 16.x fix commit (9571ec8) raises the sharp requirement in packages/next/package.json:
"optionalDependencies": {
- "sharp": "^0.35.4"
+ "sharp": "^0.35.5"
}
The 15.x fix commit (5e3b687) does the same for its range: "sharp": "^0.34.3 || ^0.35.4" becomes "^0.34.3 || ^0.35.5". Sharp 0.35.5 swaps the prebuilt libvips packages from @img/sharp-libvips-* 1.3.3 to 1.3.4. The 1.3.4 release bundles rsvg 2.63.2 (per its release notes), the earliest fixed main-branch release per RustSec, which contains upstream commit 8a1b0cd3. That commit stops freeing xmlEntity pointers libxml2 still uses. With the bug gone, the nested-XInclude document no longer produces a dangling reference, so neither the crash nor the code-execution path exists.
Is the fix complete? For the bundled-binary path, yes: the patched librsvg removes the root cause, and the advisory's version ranges match the dependency change. Two caveats remain:
- Pinned sharp versions. Pinning
sharpbelow 0.35.5, including any 0.34.x release, keeps a vulnerable librsvg even on a patched Next.js. That is why the advisory states the workaround in terms of the sharp version. - "Possible" RCE is still exposure. The advisory calls the impact "possible" RCE, since use-after-free exploitation depends on allocator behavior. Treat every affected deployment as exposed rather than triaging by likelihood.
How do I check if I was compromised?
No indicators of compromise have been published for this issue. Absent public guidance, look for these general signals around the image endpoint:
- Requests to
/_next/imagewithurlparameters pointing at unexpected hosts or paths, especially any that turned out to serve SVG content. - Bursts of HTTP 400 responses from the optimizer, which rejects SVG content when
dangerouslyAllowSVGis disabled; probing traffic can leave that trail. - Crashes or restarts of the Node.js process correlated with image optimization, since the same defect can crash the parser instead of yielding control to the attacker.
- Sharp errors in logs such as "Input buffer contains unsupported image format", which the optimizer logs when a load fails partway.
If you find suspicious activity, rotate any credentials the server process could reach, since code execution in that process can read them.
Timeline
- September 23, 2026: Rafael B. Dias reports the librsvg use-after-free to Red Hat. It is disclosed the same day alongside RUSTSEC-2026-0305 and CVE-2026-96889.
- September 25, 2026: CISA's ADP vulnrichment for CVE-2026-96889 records a public proof of concept ("Exploitation: poc", technical impact "total").
- September 27, 2026: sharp-libvips 1.3.4 ships with librsvg 2.63.2, and sharp 0.35.5 releases with it.
- October 14, 2026: Vercel publishes GHSA-2464-fhr4-w9xw and releases Next.js 15.5.28 and 16.4.1 with the sharp bump.
- October 22, 2026: The advisory is published and reviewed in the GitHub Advisory Database.
- October 23, 2026: This post. No exploitation in the wild is known; the issue is not in the CISA KEV catalog.
FAQ
Is my app exposed with the default Next.js configuration?
The Image Optimization API is enabled by default for apps using next/image with the default loader. Whether SVG content can reach sharp depends on your version's detection path and your images configuration. Upgrade regardless of configuration.
Do I have sharp installed at all?
From 15.0.0 onward, Next.js lists sharp in its optional dependencies, so a normal npm install on a supported platform includes it. Releases before 15.0.0 that we checked (12.3.0, 13.5.6, 14.2.32) do not, so sharp is present only if your project installs it. Run npm ls sharp. A 0.35.5 or later answer carries the patched librsvg 2.63.2. A 0.34.x answer does not: those releases bundle 2.60.0, 2.61.1 or 2.61.2, all older than the 2.60.3 and 2.61.5 backports.
Is there a CVE for the Next.js advisory itself? As of October 23, 2026 the GitHub advisory GHSA-2464-fhr4-w9xw has no CVE of its own. The underlying librsvg flaw is CVE-2026-96889, which is how NVD and Linux distributions track it.
My app only serves PNG and JPEG. Should I worry? The advisory scopes the flaw to SVG files used in image optimization. An app whose allowlists cannot be made to serve SVG to the optimizer is outside the described path. The safe move is still the one-line upgrade.
Does Alpine Linux (musl) protect me? The advisory scopes the RCE to glibc-based Linux. The musl prebuilds are separate packages, and the advisory makes no claim about them. Do not treat the platform difference as a fix; upgrade sharp and Next.js anyway.
Is this being exploited? No exploitation in the wild is known as of October 23, 2026, and the issue is not in the CISA KEV catalog. A public proof of concept for the librsvg bug exists, so assume interest from opportunistic attackers and patch on that basis.
Scan your Next.js repositories with Ostorlab source code scanning, then use the version checks above to verify the Next.js and sharp builds installed in each deployment.
References
- Vercel advisory GHSA-2464-fhr4-w9xw: https://github.com/vercel/next.js/security/advisories/GHSA-2464-fhr4-w9xw (checked October 23, 2026; credits finders
randomguy6407andrafabd1, checked October 27, 2026) - GitHub Advisory Database entry: https://github.com/advisories/GHSA-2464-fhr4-w9xw (checked October 23, 2026)
- CVE-2026-96889 record (Red Hat CNA): https://cveawg.mitre.org/api/cve/CVE-2026-96889 (checked October 23, 2026)
- RustSec advisory RUSTSEC-2026-0305 (patched-version matrix re-checked October 24, 2026): https://rustsec.org/advisories/RUSTSEC-2026-0305.html
- Librsvg issue tracker entry (work item #1241), which references fix commit 8a1b0cd319e9af2d1e9cf878081dd77f227a0504: https://gitlab.gnome.org/GNOME/librsvg/-/work_items/1241 (checked October 23, 2026)
- Next.js 16.x fix commit (sharp bump): https://github.com/vercel/next.js/commit/9571ec899f063df6533351bec390ccb2cc0af6a3 (checked October 23, 2026)
- Next.js 15.x fix commit (sharp bump): https://github.com/vercel/next.js/commit/5e3b687b5edb868b064bdc058d768a7021e7f6ad (checked October 23, 2026)
- Next.js 16.4.1 release notes: https://github.com/vercel/next.js/releases/tag/v16.4.1 (checked October 23, 2026)
- sharp 0.35.5 release notes: https://github.com/lovell/sharp/releases/tag/v0.35.5 (checked October 23, 2026)
- sharp 0.35.5 package metadata (@img/sharp-libvips-* 1.3.4 pin, Node engine
>=20.9.0): https://raw.githubusercontent.com/lovell/sharp/v0.35.5/package.json (checked October 27, 2026) - sharp 0.33.5 package metadata (@img/sharp-libvips-* 1.0.4 pin): https://raw.githubusercontent.com/lovell/sharp/v0.33.5/package.json (checked October 27, 2026)
- @img/sharp-libvips-linux-x64 1.0.4 published package,
versions.json(rsvg 2.58.93): https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz (checked October 27, 2026) - Next.js package metadata, tags v14.2.32 (no sharp dependency) and v15.0.0 (
"sharp": "^0.33.5"): https://github.com/vercel/next.js/blob/v14.2.32/packages/next/package.json and https://github.com/vercel/next.js/blob/v15.0.0/packages/next/package.json (checked October 27, 2026) - sharp 0.34.3 package metadata (@img/sharp-libvips-* 1.2.0 pin, Node engine range): https://raw.githubusercontent.com/lovell/sharp/v0.34.3/package.json (checked October 24, 2026)
- sharp-libvips 1.2.0 build versions (rsvg 2.60.0): https://raw.githubusercontent.com/lovell/sharp-libvips/v1.2.0/versions.properties (checked October 24, 2026)
- sharp-libvips 1.2.3 build versions (rsvg 2.61.1): https://raw.githubusercontent.com/lovell/sharp-libvips/v1.2.3/versions.properties (checked October 24, 2026)
- sharp-libvips 1.2.4 build versions (rsvg 2.61.2): https://raw.githubusercontent.com/lovell/sharp-libvips/v1.2.4/versions.properties (checked October 24, 2026)
- sharp-libvips 1.3.4 release notes (rsvg 2.63.2): https://github.com/lovell/sharp-libvips/releases/tag/v1.3.4 (checked October 23, 2026)
- sharp-libvips 1.3.3 release notes (rsvg 2.62.91): https://github.com/lovell/sharp-libvips/releases/tag/v1.3.3 (checked October 23, 2026)
- Next.js optimizer source, tag v16.4.0: https://github.com/vercel/next.js/blob/v16.4.0/packages/next/src/server/image-optimizer/transform.ts (checked October 23, 2026)
- Next.js optimizer source, tag v14.2.32: https://github.com/vercel/next.js/blob/v14.2.32/packages/next/src/server/image-optimizer.ts (checked October 23, 2026)
- npm download counts for
next(44,586,009 downloads, week of October 16-22, 2026): https://api.npmjs.org/downloads/point/last-week/next (checked October 24, 2026) - npm package metadata documentation (
enginesis advisory unlessengine-strictis set): https://docs.npmjs.com/cli/v11/configuring-npm/package-json/#engines (checked October 28, 2026)