On 14 October 2026, Next.js released versions 16.4.1 and 15.5.28 out of band. They close two critical vulnerabilities that let attackers execute code without authentication, plus a high-severity DoS. Before updating anything, I checked three of my container instances: the demo and support instances of my ticketing system Ticketry and an intranet project. By the conditions in the advisories, exactly one was affected, and by the least dramatic of the three flaws. I updated all of them anyway.
What was fixed on 14 October
Three flaws, three conditions
Each flaw only applies under its own condition. The CVSS scores come from the project's GitHub advisories:
use cache |
SVG image optimization | Server Actions | |
|---|---|---|---|
| ID | GHSA-673q-mjm6-r5xr | CVE-2026-96889 | GHSA-p3m5-59mp-jv5r |
| Impact, CVSS 4.0 | code execution, 9.5 | code execution, 9.5 | DoS, 8.2 |
| Condition | App Router with Cache Components, use cache returns multiple typed arrays |
glibc-based Linux | App Router with a Server Action |
| Workaround | none | sharp 0.35.5 or later |
none |
| Fixed in | 16.4.1 | 16.4.1, 15.5.28 | 16.4.1, 15.5.28 |
The SVG flaw sits in librsvg, which sharp ships with; according to its own advisory, sharp 0.35.5 ships librsvg 2.63.2. The Pages Router and applications without Server Actions are not affected by the DoS. The downtime cost calculator shows what a blocked process costs per hour. For the use cache flaw, Next.js advises updating immediately even without such functions. Lines such as 14.x fall within the affected ranges of the other two flaws but get no fix.
Fixes from 16.4.1 and 15.5.28 on
According to the support policy, 16.x is Active LTS and 15.x is Maintenance LTS. That phase only brings critical bug fixes and security updates and lasts two years from the initial release. Next.js 15 came out on 21 October 2024, so the phase ends on 21 October 2026. There is no 16.3 release with the fixes. Version 16.4.2 from 15 October contains them as well and otherwise only brings backported corrections, for example for Turbopack. The target is therefore 16.4.1 or later.
Five questions per instance
Five questions decide whether an instance is affected: Which next version is running? Are there Server Actions? Is use cache active with Cache Components? Does the container run on glibc or musl? And which sharp version does next get?
Searching the code
grep in the app directory finds Server Actions and use cache:
# run in the app directory (where next.config.* lives)
count() {
grep -rlE --include='*.[jt]s' --include='*.[jt]sx' \
--exclude-dir=node_modules --exclude-dir=.next -e "$1" . | wc -l
}
echo "Server Actions: $(count "^[[:space:]]*['\"]use server['\"]")"
echo "use cache: $(count "['\"]use cache")"
echo "next/image: $(count "from ['\"]next/image['\"]")"
grep -n cacheComponents next.config.* || echo "cacheComponents: not set"
In the intranet project, this returned:
Server Actions: 20
use cache: 0
next/image: 0
cacheComponents: not set
Without cacheComponents, use cache is not active according to the docs. The next/image line is only a hint, because the advisory ties the SVG flaw to glibc, not to the component.
Reading versions and the C library
A Node script answers the rest. It resolves next from the app directory and sharp from next, whose optional dependency it is. The C library comes from detect-libc, which sharp uses to pick its binary. Since sharp does not export its package.json, the script searches upwards from the entry point:
// next-gate.mjs: fails if next, or the sharp that next loads, is below
// the fixed versions of 2026-10-14. Run it in the app directory.
import { createRequire } from 'node:module';
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
const MIN_NEXT = { 15: '15.5.28', 16: '16.4.1' };
const MIN_SHARP = '0.35.5';
const parts = (v) => v.split('-')[0].split('.').map(Number);
// pre-releases such as 0.35.5-rc.1 count as older than the release
const older = (v, min) => {
const [a, b] = [parts(v), parts(min)];
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i];
return v.includes('-');
};
// sharp does not export its package.json, so search upwards from the entry point
const versionOf = (entry, name) => {
for (let dir = dirname(entry); dir !== dirname(dir); dir = dirname(dir)) {
const file = join(dir, 'package.json');
if (!existsSync(file)) continue;
const pkg = JSON.parse(readFileSync(file, 'utf8'));
if (pkg.name === name) return pkg.version;
}
throw new Error(`package.json of ${name} not found`);
};
const nextEntry = createRequire(join(process.cwd(), 'package.json')).resolve('next');
const next = versionOf(nextEntry, 'next');
// resolve sharp from next: sharp is an optional dependency of next
let sharpEntry;
try {
sharpEntry = createRequire(nextEntry).resolve('sharp');
} catch {
console.error('Gate: sharp not found');
process.exit(1);
}
const sharp = versionOf(sharpEntry, 'sharp');
const libc = createRequire(sharpEntry)('detect-libc').familySync();
const errors = [];
const major = parts(next)[0];
if (next.includes('-')) errors.push(`next ${next} is a pre-release`);
else if (major < 15) errors.push(`next ${next}: line without a fix`);
else if (MIN_NEXT[major] && older(next, MIN_NEXT[major])) errors.push(`next ${next} < ${MIN_NEXT[major]}`);
if (older(sharp, MIN_SHARP)) errors.push(`sharp ${sharp} < ${MIN_SHARP}`);
console.log(`next ${next}, sharp ${sharp}, libc ${libc ?? 'unknown'}`);
if (errors.length) {
console.error(`Gate: ${errors.join('; ')}`);
process.exit(1);
}
After the update, it reports this in the intranet project's checkout on the host:
next 16.4.2, sharp 0.35.5, libc glibc
The glibc value comes from the host here; in the Alpine image, detect-libc reports musl. ps is tempting too, because Next.js sets the process title to next-server (v<version>). After the update, my host showed:
$ ps -eo args | grep '[n]ext-server'
next-server (v
next-server (v16.4.2)
next-server (v16.4.2)
next-server (v16.4.2)
The first line belongs to a fourth, older instance that starts with node server.js. Node.js writes the title into the memory of the original command line, and here that only holds 14 characters. The three instances I checked start with node apps/web/server.js. So the process title is no reliable proof of the version.
Result for my three instances
| Ticketry demo | Ticketry support | Intranet project | |
|---|---|---|---|
next before |
16.3.4 | 16.3.4 | 16.4.0 |
| Server Actions | none | none | 20 files |
use cache |
no | no | no |
| Runner | node:24-alpine |
node:24-alpine |
node:24-alpine |
sharp before |
0.35.4 | 0.35.4 | 0.35.5 (override) |
| Advisory condition met | none | none | DoS |
Only the intranet instance has Server Actions and was therefore affected by the DoS. No instance met the glibc condition of the SVG flaw, since all runners run on Alpine with musl.
Not affected does not mean done
I updated the Ticketry instances anyway: 16.3.4 sits on a line without a fix, and that version already lacked the corrections of the September release 16.3.8. The new image tag goes into the .env, followed by docker compose pull and docker compose up -d. The up restarts production, so I only release it after a second look. According to my audit, the fourth instance meets none of the three conditions, but it also sits on a line without a fix and moves to 16.4 with its next deploy.
Why I don't wait for npm audit
An audit would not have reported any of the three flaws for the intranet instance. Since version 7, npm queries the registry's Bulk Advisory endpoint. On 16 October at 02:00 CEST, it returned an empty result for the intranet instance's versions before the update:
curl -s -X POST -H 'content-type: application/json' \
-d '{"next":["16.4.0"],"sharp":["0.35.5"]}' \
https://registry.npmjs.org/-/npm/v1/security/advisories/bulk
{}
With sharp 0.35.4, the state of the Ticketry instances, it listed GHSA-wq5f-xc86-pv6w, but still nothing for next 16.4.0. The three Next.js advisories were also missing from the global GitHub Advisory Database, whose API answered with 404. According to GitHub, it may take time for new vulnerabilities to appear there and trigger alerts. The triage matrix bridges that time.
The script as a build gate
Placing it in the builder stage
The script exits with code 1 if next or sharp is too old, next runs on a line without a fix or as a pre-release, or sharp is missing. In the builder stage, it goes between installation and build:
FROM node:24-alpine AS builder
WORKDIR /app
# ... install dependencies and copy the source as before
RUN node scripts/next-gate.mjs
RUN npm run build
In a monorepo it has to run in the app's directory, because it resolves next from there. My builder and runner share the same base, so the libc line also holds at runtime. A counter-check with an artificially outdated node_modules shows a trap in the 15 line:
next 15.5.28, sharp 0.34.4, libc glibc
Gate: sharp 0.34.4 < 0.35.5
According to its package.json, next 15.5.28 still allows sharp ^0.34.3. A 0.34 version held in the lockfile can therefore survive the update and stay below 0.35.5, the version from which sharp lists the flaw as fixed.
Why the runners stay on Alpine
In my post on the multi-stage Dockerfile for Node and .NET, I suggest node:24-slim when native modules cause trouble on Alpine. Here that would be the wrong reflex: the Debian image meets the glibc condition and is larger. The values come from the Docker Hub API (field size, linux/amd64, compressed, as of 16 October):
node:24-alpine |
node:24-slim |
|
|---|---|---|
| C library | musl | glibc (Debian 12) |
| Compressed size | 62.4 MB | 81.0 MB |
sharp binary |
linuxmusl-x64 |
linux-x64 |
| Meets the SVG condition | no | yes |
Still, I don't count Alpine as protection. The other two flaws are not tied to any platform, and no advisory declares musl safe. That is why the gate checks sharp regardless of the libc, so that a later switch to Debian does not silently bring the flaw back.
What went wrong while patching
pnpm 11 and the silent exception
Ticketry uses pnpm 11. Since version 11, pnpm by default only installs versions that are at least 1440 minutes old, as long as the requested range allows one. This built-in cooldown is not strict, though: if no sufficiently old version satisfies the range, as with the exactly pinned next@16.4.2, pnpm installs the younger one anyway and adds it to minimumReleaseAgeExclude in pnpm-workspace.yaml. The update added ten entries this way:
minimumReleaseAgeExclude:
- '@next/env@16.4.2'
- '@next/swc-linux-x64-musl@16.4.2'
# ... seven more @next/swc-* for other platforms
- next@16.4.2
At the release commit on 16 October at 00:18 CEST, 16.4.2 was less than an hour and a half old, while 16.4.1 with the actual fixes was already a good 28 hours old. I committed the exceptions deliberately and explained them. For the security fixes they would not have been necessary, because 16.4.1 had already passed the cooldown, and the cooldown is meant to protect against exactly such young releases. For security updates I therefore recommend pinning the version with the fix and setting the cooldown explicitly. pnpm then works strictly: if it finds no sufficiently old version, it prompts in the terminal and, without a terminal, for example in CI, aborts with ERR_PNPM_NO_MATURE_MATCHING_VERSION.
# pnpm-workspace.yaml
minimumReleaseAge: 1440
# remove exceptions the lockfile no longer needs (pnpm 11.22.0 or later)
minimumReleaseAgeExcludePrune: true
Type errors from a stale .next/types
In the intranet project, tsc --noEmit reported errors like these after the update:
src/lib/manage/areas.ts(59,3): error TS2322: Type '"/manage/events"' is not assignable to type 'Route'.
.next/types/validator.ts(350,39): error TS2307: Cannot find module '../../src/app/api/revalidate/route.js' or its corresponding type declarations.
They had nothing to do with the update. The project uses typedRoutes, and .next/types came from an older build: the route /api/revalidate, removed on 24 September, was still referenced, while the page /manage/events, added on 13 October, was missing. tsc does not generate these types; next dev, next build and, since version 15.5, next typegen do. Afterwards the type check ran without errors:
pnpm exec next typegen
pnpm exec tsc --noEmit
Quick answers: Next.js 16.4.1
Is upgrading sharp to 0.35.5 enough?
Only against the SVG flaw. There is no workaround for the use cache flaw, and according to the advisory, only the update helps against the DoS.
I only use the Pages Router. Do I need to update?
Yes. The DoS and the use cache flaw require the App Router, but for the SVG flaw the advisory names no router condition, only glibc.
What about Next.js 15?
15.5.28 closes the SVG flaw and the DoS; the use cache flaw only affects 16.x. Check sharp explicitly there and plan the move to 16, because the Maintenance phase ends on 21 October 2026.
Dates and sources
What comes next
For 21 October or shortly after, the Node.js project has announced security releases for 26.x, 24.x and 22.x, with HIGH as the highest severity. Then it is the base images' turn, as described in the post on the Node.js security updates from June; a new image behind a tag such as node:24-alpine is reported, for example, by Diun instead of Watchtower. If you don't want to track such updates yourself: I handle them as part of IT support for SMEs.
Advisories and documentation
- Next.js Security Update of 14 October 2026
- GHSA-673q-mjm6-r5xr, GHSA-2464-fhr4-w9xw, GHSA-p3m5-59mp-jv5r and the
sharpadvisory GHSA-wq5f-xc86-pv6w - Next.js Support Policy and
next typegen - pnpm: minimumReleaseAge and the release notes of pnpm 11.1.3 with the strict mode
- Node.js: October 2026 Security Releases
Note: The articles on this blog are produced with the help of AI and are editorially reviewed before publication. Editorial responsibility lies with Emre Yurtbay (see the legal notice).