Am 14. Oktober 2026 hat Next.js außerplanmäßig die Versionen 16.4.1 und 15.5.28 veröffentlicht. Sie schließen zwei kritische Lücken, über die Angreifer ohne Anmeldung Code ausführen können, und eine DoS-Lücke mit hohem Schweregrad. Bevor ich etwas aktualisiert habe, habe ich drei meiner Container-Instanzen geprüft: die Demo- und die Support-Instanz meines Ticketsystems Ticketry und ein Intranet-Projekt. Nach den Bedingungen der Advisories war genau eine betroffen, und zwar von der am wenigsten dramatischen Lücke. Aktualisiert habe ich trotzdem alle.
Was am 14. Oktober geschlossen wurde
Drei Lücken, drei Bedingungen
Jede Lücke greift nur unter einer eigenen Bedingung. Die CVSS-Werte stammen aus den GitHub-Advisories des Projekts:
use cache |
SVG-Bildoptimierung | Server Actions | |
|---|---|---|---|
| Kennung | GHSA-673q-mjm6-r5xr | CVE-2026-96889 | GHSA-p3m5-59mp-jv5r |
| Wirkung, CVSS 4.0 | Codeausführung, 9,5 | Codeausführung, 9,5 | DoS, 8,2 |
| Bedingung | App Router mit Cache Components, use cache liefert mehrere Typed Arrays |
glibc-basiertes Linux | App Router mit Server Action |
| Workaround | keiner | sharp ab 0.35.5 |
keiner |
| Behoben in | 16.4.1 | 16.4.1, 15.5.28 | 16.4.1, 15.5.28 |
Die SVG-Lücke steckt in librsvg, das sharp mitbringt; sharp 0.35.5 liefert laut eigenem Advisory librsvg 2.63.2 aus. Pages Router und Anwendungen ohne Server Actions sind von der DoS-Lücke nicht betroffen. Was ein blockierter Prozess pro Stunde kostet, zeigt der Ausfallkosten-Rechner. Zur use cache-Lücke rät Next.js, auch ohne solche Funktionen sofort zu aktualisieren. Linien wie 14.x liegen in den betroffenen Bereichen der beiden anderen Lücken, bekommen aber keinen Fix.
Fixes ab 16.4.1 und 15.5.28
Laut Support-Policy ist 16.x Active LTS und 15.x Maintenance LTS. Diese Phase bringt nur noch kritische Fehler- und Sicherheitskorrekturen und dauert zwei Jahre ab dem ersten Release. Next.js 15 erschien am 21. Oktober 2024, die Phase endet also am 21. Oktober 2026. Ein 16.3-Release mit den Fixes gibt es nicht. Version 16.4.2 vom 15. Oktober enthält sie ebenfalls und bringt darüber hinaus nur zurückportierte Korrekturen, etwa für Turbopack. Ziel ist deshalb 16.4.1 oder neuer.
Fünf Fragen je Instanz
Ob eine Instanz betroffen ist, klären fünf Fragen: Welche next-Version läuft? Gibt es Server Actions? Ist use cache mit Cache Components aktiv? Läuft der Container auf glibc oder musl? Und welche sharp-Version bekommt next?
Den Code durchsuchen
Server Actions und use cache findet grep im App-Verzeichnis:
# im App-Verzeichnis ausfuehren (dort, wo next.config.* liegt)
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: nicht gesetzt"
Im Intranet-Projekt ergab das:
Server Actions: 20
use cache: 0
next/image: 0
cacheComponents: nicht gesetzt
Ohne cacheComponents ist use cache laut Doku nicht aktiv. Die Zeile für next/image ist nur ein Hinweis, denn das Advisory knüpft die SVG-Lücke an glibc, nicht an die Komponente.
Versionen und C-Bibliothek auslesen
Den Rest beantwortet ein Node-Skript. Es löst next aus dem App-Verzeichnis auf und sharp von next aus, dessen optionale Abhängigkeit es ist. Die C-Bibliothek liefert detect-libc, mit dem sharp seine Binärdatei wählt. Da sharp seine package.json nicht exportiert, sucht das Skript sie vom Einstiegspunkt aufwärts:
// next-gate.mjs: bricht ab, wenn next oder das von next geladene sharp
// unter dem Fix-Stand vom 14.10.2026 liegt. Aufruf im App-Verzeichnis.
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);
// Vorabversionen wie 0.35.5-rc.1 zaehlen als aelter als das 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 exportiert seine package.json nicht, daher vom Einstieg aufwaerts suchen
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 von ${name} nicht gefunden`);
};
const nextEntry = createRequire(join(process.cwd(), 'package.json')).resolve('next');
const next = versionOf(nextEntry, 'next');
// sharp von next aus aufloesen: sharp ist eine optionale Abhaengigkeit von next
let sharpEntry;
try {
sharpEntry = createRequire(nextEntry).resolve('sharp');
} catch {
console.error('Gate: sharp nicht gefunden');
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} ist eine Vorabversion`);
else if (major < 15) errors.push(`next ${next}: Linie ohne 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 ?? 'unbekannt'}`);
if (errors.length) {
console.error(`Gate: ${errors.join('; ')}`);
process.exit(1);
}
Nach dem Update meldet es im Checkout des Intranet-Projekts auf dem Host:
next 16.4.2, sharp 0.35.5, libc glibc
Die Angabe glibc stammt hier vom Host, im Alpine-Image meldet detect-libc musl. Verlockend ist auch ps, weil Next.js den Prozesstitel auf next-server (v<Version>) setzt. Auf meinem Host sah das nach dem Update so aus:
$ 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)
Die erste Zeile gehört zu einer vierten, älteren Instanz, die mit node server.js startet. Node.js schreibt den Titel in den Speicher der ursprünglichen Kommandozeile, und der reicht hier nur für 14 Zeichen. Die drei geprüften Instanzen starten mit node apps/web/server.js. Als Versionsnachweis taugt der Prozesstitel also nicht.
Ergebnis bei meinen drei Instanzen
| Ticketry Demo | Ticketry Support | Intranet-Projekt | |
|---|---|---|---|
next vorher |
16.3.4 | 16.3.4 | 16.4.0 |
| Server Actions | keine | keine | 20 Dateien |
use cache |
nein | nein | nein |
| Runner | node:24-alpine |
node:24-alpine |
node:24-alpine |
sharp vorher |
0.35.4 | 0.35.4 | 0.35.5 (Override) |
| Advisory-Bedingung erfüllt | keine | keine | DoS |
Nur die Intranet-Instanz hat Server Actions und war damit von der DoS-Lücke betroffen. Die glibc-Bedingung der SVG-Lücke erfüllte keine Instanz, alle Runner laufen auf Alpine mit musl.
Nicht betroffen heißt nicht erledigt
Die Ticketry-Instanzen habe ich trotzdem aktualisiert: 16.3.4 liegt auf einer Linie ohne Fix, und dieser Version fehlten schon die Korrekturen des September-Releases 16.3.8. Der neue Image-Tag kommt in die .env, dann folgen docker compose pull und docker compose up -d. Das up startet die Produktion neu, deshalb gebe ich es erst nach einem zweiten Blick frei. Die vierte Instanz erfüllt laut meinem Audit keine der drei Bedingungen, liegt aber ebenfalls auf einer Linie ohne Fix und kommt mit dem nächsten Deploy auf 16.4.
Warum ich nicht auf npm audit warte
Ein Audit hätte bei der Intranet-Instanz keine der drei Lücken gemeldet. npm fragt seit Version 7 den Bulk-Advisory-Endpunkt der Registry ab. Am 16. Oktober um 02:00 Uhr lieferte er für die Versionen der Intranet-Instanz vor dem Update ein leeres Ergebnis:
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
{}
Mit sharp 0.35.4, dem Stand der Ticketry-Instanzen, nannte er GHSA-wq5f-xc86-pv6w, für next 16.4.0 aber weiterhin nichts. Auch in der globalen GitHub Advisory Database fehlten die drei Next.js-Advisories, die API antwortete mit 404. Laut GitHub kann es dauern, bis neue Schwachstellen dort erscheinen und Alerts auslösen. Diese Zeit überbrückt die Prüfmatrix.
Das Skript als Build-Gate
Einbau in die Builder-Stage
Das Skript endet mit Exit-Code 1, wenn next oder sharp zu alt sind, next auf einer Linie ohne Fix oder als Vorabversion läuft oder sharp fehlt. In der Builder-Stage gehört es zwischen Installation und Build:
FROM node:24-alpine AS builder
WORKDIR /app
# ... Abhaengigkeiten installieren und Quellcode kopieren wie bisher
RUN node scripts/next-gate.mjs
RUN npm run build
Im Monorepo muss es im Verzeichnis der App laufen, weil es next von dort auflöst. Builder und Runner haben bei mir dieselbe Basis, die libc-Zeile gilt also auch zur Laufzeit. Eine Gegenprobe mit künstlich veraltetem node_modules zeigt eine Falle der 15er-Linie:
next 15.5.28, sharp 0.34.4, libc glibc
Gate: sharp 0.34.4 < 0.35.5
next 15.5.28 erlaubt laut seiner package.json weiterhin sharp ^0.34.3. Eine im Lockfile festgehaltene 0.34er-Version kann das Update also überstehen und bleibt unter 0.35.5, ab der sharp die Lücke als behoben führt.
Warum die Runner auf Alpine bleiben
In meinem Beitrag zum Multi-Stage-Dockerfile für Node und .NET rate ich zu node:24-slim, wenn native Module auf Alpine Ärger machen. Hier wäre das der falsche Reflex: Das Debian-Image erfüllt die glibc-Bedingung und ist größer. Die Werte stammen aus der Docker-Hub-API (Feld size, linux/amd64, komprimiert, Stand 16. Oktober):
node:24-alpine |
node:24-slim |
|
|---|---|---|
| C-Bibliothek | musl | glibc (Debian 12) |
| Größe komprimiert | 62,4 MB | 81,0 MB |
sharp-Binärdatei |
linuxmusl-x64 |
linux-x64 |
| SVG-Bedingung erfüllt | nein | ja |
Als Schutz verbuche ich Alpine trotzdem nicht. Die beiden anderen Lücken hängen an keiner Plattform, und kein Advisory erklärt musl für sicher. Deshalb prüft das Gate sharp unabhängig von der libc, damit ein späterer Wechsel auf Debian die Lücke nicht still zurückbringt.
Was beim Patchen schiefging
pnpm 11 und die stille Ausnahme
Ticketry nutzt pnpm 11. Seit Version 11 installiert pnpm standardmäßig nur Versionen, die mindestens 1440 Minuten alt sind, sofern der angeforderte Bereich eine solche zulässt. Dieser eingebaute Cooldown ist aber nicht strikt: Erfüllt keine ausreichend alte Version den Bereich, wie beim exakt gepinnten next@16.4.2, installiert pnpm die jüngere trotzdem und trägt sie in minimumReleaseAgeExclude der pnpm-workspace.yaml ein. Beim Update kamen so zehn Einträge hinzu:
minimumReleaseAgeExclude:
- '@next/env@16.4.2'
- '@next/swc-linux-x64-musl@16.4.2'
# ... sieben weitere @next/swc-* fuer andere Plattformen
- next@16.4.2
Beim Release-Commit am 16. Oktober um 00:18 Uhr war 16.4.2 keine anderthalb Stunden alt, 16.4.1 mit den eigentlichen Fixes schon gut 28 Stunden. Ich habe die Ausnahmen bewusst committet und begründet. Für die Sicherheitsfixes wären sie nicht nötig gewesen, denn 16.4.1 hatte die Frist schon hinter sich, und genau vor so jungen Releases soll der Cooldown schützen. Für Sicherheitsupdates empfehle ich deshalb, die Version mit dem Fix zu pinnen und den Cooldown ausdrücklich zu setzen. Dann arbeitet pnpm strikt: Findet es keine ausreichend alte Version, fragt es im Terminal nach und bricht ohne Terminal, etwa in CI, mit ERR_PNPM_NO_MATURE_MATCHING_VERSION ab.
# pnpm-workspace.yaml
minimumReleaseAge: 1440
# Ausnahmen entfernen, die das Lockfile nicht mehr braucht (ab pnpm 11.22.0)
minimumReleaseAgeExcludePrune: true
Typfehler aus einem veralteten .next/types
Im Intranet-Projekt meldete tsc --noEmit nach dem Update Fehler wie diese:
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.
Mit dem Update hatten sie nichts zu tun. Das Projekt nutzt typedRoutes, und .next/types stammte aus einem älteren Build: Die am 24. September entfernte Route /api/revalidate war noch referenziert, die am 13. Oktober hinzugekommene Seite /manage/events fehlte. tsc erzeugt diese Typen nicht, das tun next dev, next build und seit Version 15.5 next typegen. Danach lief der Typecheck fehlerfrei:
pnpm exec next typegen
pnpm exec tsc --noEmit
Kurz gefragt: Next.js 16.4.1
Reicht es, sharp auf 0.35.5 anzuheben?
Nur gegen die SVG-Lücke. Gegen die use cache-Lücke gibt es keinen Workaround, gegen die DoS-Lücke hilft laut Advisory nur das Update.
Ich nutze nur den Pages Router. Muss ich aktualisieren?
Ja. Die DoS- und die use cache-Lücke setzen den App Router voraus, für die SVG-Lücke nennt das Advisory aber keine Router-Bedingung, nur glibc.
Was gilt für Next.js 15?
15.5.28 schließt die SVG- und die DoS-Lücke, die use cache-Lücke betrifft nur 16.x. Prüfen Sie sharp dort ausdrücklich und planen Sie den Umstieg auf 16, denn die Maintenance-Phase endet am 21. Oktober 2026.
Termine und Quellen
Was als Nächstes ansteht
Für den 21. Oktober oder kurz danach hat das Node.js-Projekt Sicherheitsreleases für 26.x, 24.x und 22.x angekündigt, höchster Schweregrad HIGH. Dann sind die Basis-Images dran, wie im Beitrag zu den Node.js-Sicherheitsupdates vom Juni beschrieben; ein neues Image hinter einem Tag wie node:24-alpine meldet zum Beispiel Diun statt Watchtower. Wer solche Updates nicht selbst nachhalten möchte: Das übernehme ich im Rahmen von IT-Support für KMU.
Advisories und Dokumentation
- Next.js Security Update vom 14. Oktober 2026
- GHSA-673q-mjm6-r5xr, GHSA-2464-fhr4-w9xw, GHSA-p3m5-59mp-jv5r und das
sharp-Advisory GHSA-wq5f-xc86-pv6w - Next.js Support Policy und
next typegen - pnpm: minimumReleaseAge und die Release Notes zu pnpm 11.1.3 mit dem strikten Modus
- Node.js: October 2026 Security Releases
Hinweis: Die Beiträge dieses Blogs werden unter Einsatz von KI erstellt und vor der Veröffentlichung redaktionell geprüft. Die redaktionelle Verantwortung trägt Emre Yurtbay (siehe Impressum).