On October 14, Next.js shipped an out-of-band security release covering three vulnerabilities in upstream dependencies: two rated Critical, one High. The advice from every vendor is the same sentence: upgrade to 15.5.28 or 16.4.1 and redeploy. That advice is correct. It is also incomplete in a way that will leave some production apps exposed to a CVSS 9.5 remote code execution bug while their owners believe they patched it.
The thesis of this issue is narrow. A version bump of a framework is not a version bump of the code your framework executes. The three advisories show three different ways those two things diverge: a transitive native dependency that a lockfile can pin below the fix, a bundled copy of a library that react upgrades will not touch, and an advisory pipeline that did not yet reach npm audit.
What actually shipped
The Next.js announcement on October 8 said an update was coming on October 14 for two Critical and one High issue, and that two of the fixes had been postponed from September because of upstream coordination. That phrase matters. These are not bugs in Next.js application code. They live in dependencies Next.js ships or reaches into.
The first, GHSA-673q-mjm6-r5xr, is an unauthenticated RCE in use cache for React Server Components. It affects Next.js 16.0.0 through 16.4.0 and the react-server-dom-* packages 19.0.0 through 19.3.0, and it needs an App Router app with Cache Components enabled and a use cache function that returns more than one typed array. React's own advisory describes it as an extremely rare case where responses transport multiple typed arrays and an attacker-controlled value. CVSS 9.5. There is no workaround.
The second, GHSA-2464-fhr4-w9xw (CVE-2026-96889), is an unauthenticated RCE in image optimization when SVG files are involved. A memory bug in librsvg, which sharp uses to decode SVG, can lead to code execution on glibc-based Linux. The GitHub advisory lists affected versions from 10.0.0 through 15.5.27 and 16.0.0 through 16.4.0. That is six years of Next.js. CVSS 9.5 again.
The third, GHSA-p3m5-59mp-jv5r, is a denial of service in App Router Server Actions: crafted requests can pin a server process at full CPU. It reaches back to 13.0.0 and needs only one Server Action in the app. No workaround either.
The fix that is two fixes
Look at the workaround line in the SVG advisory. It says to make sure the version of sharp that Next.js uses is 0.35.5 or later, that fresh installs get a fixed sharp automatically, and that upgrading Next.js lets package managers provide a fixed sharp.
Read the weasel word: lets. Next.js declares sharp as an optional dependency with a semver range. Upgrading next changes what a fresh resolution would pick. It does not force an existing lockfile entry forward. A lockfile that already resolved [email protected] keeps resolving it, because that version still satisfies the range. Per the reporting on the release, the 15.5.28 backport can still accept the affected 0.34.x line, so on the 15.x branch the lockfile has to be moved by hand.
So the sequence a tired engineer will run on a 15.x app is npm install [email protected], watch CI go green, deploy, and close the ticket. At the end of that sequence npm ls next reports a patched framework, and npm ls sharp can still report a decoder that is below the fix. The bump satisfied the advisory's version table and missed the advisory's actual defect, which is in a library the table never mentions.
I wrote the check as a pure function over the two versions so the logic is testable without installing anything. The ranges are copied from the advisories.
const parse = v => v.split('.').map(Number);
const cmp = (a, b) => {
const [x, y] = [parse(a), parse(b)];
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i];
return 0;
};
const inRange = (v, lo, hi) => cmp(v, lo) >= 0 && cmp(v, hi) <= 0;
export function audit({ next, sharp }) {
const findings = [];
if (inRange(next, '10.0.0', '15.5.27') || inRange(next, '16.0.0', '16.4.0'))
findings.push('GHSA-2464-fhr4-w9xw: next is in the vulnerable range');
if (cmp(sharp, '0.35.5') < 0)
findings.push(`sharp ${sharp} < 0.35.5: bundled decoder still unpatched`);
if (inRange(next, '16.0.0', '16.4.0'))
findings.push('GHSA-673q-mjm6-r5xr: use cache RCE range');
if (inRange(next, '13.0.0', '15.5.27') || inRange(next, '16.0.0', '16.4.0'))
findings.push('GHSA-p3m5-59mp-jv5r: Server Actions DoS range');
return findings;
}Running it over four lockfile states gives this output, which is real output from a Node run, not an illustration:
before ["GHSA-2464-fhr4-w9xw: next is in the vulnerable range","sharp 0.34.3 < 0.35.5: bundled decoder still unpatched","GHSA-p3m5-59mp-jv5r: Server Actions DoS range"]
bumped next only ["sharp 0.34.3 < 0.35.5: bundled decoder still unpatched"]
bumped both []
16 patched []The second row is the one to stare at. After bumping only next to 15.5.28, two of the three findings disappear and one stays. The framework is clean. The decoder is not. Whether that residual finding is exploitable in your app depends on whether untrusted SVG ever reaches sharp, which brings us to exposure.
Who is actually exposed
Next.js does not optimize SVG by default. The image optimizer needs images.dangerouslyAllowSVG: true before it will touch an SVG, so the clearest exposure is that flag. But the advisory text says the problem is not limited to that setting, and there is a second route: next/og. If you render ImageResponse on the Node.js runtime from user-controlled input, sharp and librsvg are in your request path whether or not you ever enabled the image optimizer.
This is the same surface that produced an earlier flaw in September, where attacker-controlled text could alter the SVG that next/og generates, and an August flaw in libheif reached through sharp via AVIF. Three months, three vulnerabilities in the same native image pipeline. The pattern is not bad luck. Image decoders are large C libraries parsing hostile binary formats, and every JavaScript framework that offers on-demand image transforms has quietly taken ownership of that attack surface.
Platform context matters for severity. Netlify's advisory page says its Image CDN serves the next/image path, so that route never runs inside a Netlify Function, and that apps rendering untrusted input through ImageResponse hit a crashed invocation rather than code execution. That is a host-specific mitigation, not a property of the bug. A self-hosted container on glibc Debian or Ubuntu has none of that padding. Alpine, which uses musl, is outside the described condition.
There is an interim control that does not require waiting for a clean install. The sharp library lets you refuse a decoder outright:
import sharp from 'sharp';
// Refuse to decode SVG input anywhere this sharp instance is used.
sharp.block({ operation: ['VipsForeignLoadSvg'] });If your product never needs to rasterize an SVG, this is a stronger position than patching, because it removes the code path instead of trusting the fix. Turn off dangerouslyAllowSVG for the same reason.
The advisory pipeline is part of your attack surface
The detail I keep returning to is from the reporting at The Hacker News: as of October 15, the use cache and Server Actions advisories had no CVE and were absent from the npm advisory feed, so npm audit may not flag them.
Think about what that means operationally. Most teams' answer to "are we vulnerable" is a dashboard fed by a CVE database or the npm audit endpoint. For a window of at least a day, two of three issues, including a Critical unauthenticated RCE, were visible only as GitHub security advisories with GHSA identifiers. A scanner that keys on CVE IDs would have reported a clean bill of health for an app running 16.4.0 with Cache Components.
That window closes eventually and the feeds catch up. The lesson is not that scanners are useless. It is that a green audit means "no known advisory in the feeds I subscribe to," which is a weaker statement than "no known vulnerability," and the gap is widest in the first days after an out-of-band release, which is exactly when attackers read the diffs.
A related trap is that RSC code is bundled. Next.js ships its own copy of react-server-dom-webpack and react-server-dom-turbopack. Running npm update react does nothing for the use cache bug, because the vulnerable code is not the react package you control. Only upgrading next replaces it.
The strongest objection
The reasonable pushback is that this is overblown. The 15.x backport might resolve sharp correctly on most fresh installs, the advisory says fresh installs get a fixed version, and the interesting RCE conditions are narrow: glibc, SVG enabled or next/og with untrusted input, and no public sign of exploitation as of October 15. A team on Alpine with default image settings is probably fine regardless.
I agree with most of that, and the conditions genuinely do reduce the number of exposed apps. But it concedes my point rather than defeating it. "Probably fine because of an unrelated base-image choice" is how a CVSS 9.5 bug survives in an organization for a year. And the cost asymmetry is lopsided: confirming the resolved sharp version takes one command, while discovering you were wrong takes an incident.
Monday morning
Do not stop at npm ls next. Run npm ls next sharp and read the second line. On 15.x, if sharp is below 0.35.5, run npm install [email protected], and in a monorepo pin it with an overrides entry so no workspace can regress it. Then verify the artifact that actually runs, not your laptop: exec into the production container and print both versions from node_modules, because a cached Docker layer can ship an old lockfile resolution under a new package.json.
Then decide about SVG on purpose. If nobody needs server-side SVG rasterization, block the decoder and remove dangerouslyAllowSVG. And subscribe to GitHub security advisories for the frameworks you run, since the CVE-keyed feeds lag them. Delete old deploy previews as well: Netlify notes they stay vulnerable until removed.

