Node2Shell und die Frage danach: Wie du Node.js installierst, verwaltest und aktuell hältst

Die Brotli-Lücke in Node.js kurz erklärt, die Parallele zu PHP2Shell — und alle Wege, Node.js zu installieren und aktuell zu halten, von NodeSource bis Docker.

von ·

Am selben Mittwoch, an dem PHP seine Patches gegen PHP2Shell veröffentlicht hat, hat auch Node.js eine kritische Lücke geschlossen: einen Fehler im Brotli-Decoder, über den präparierte komprimierte Daten Code ausführen können. Inzwischen hat er einen Spitznamen, „Node2Shell", und am 23. Oktober hat Aikido (öffnet in neuem Tab) (englisch) gezeigt, dass derselbe Decoder auch in verbreiteten Python-Paketen steckt.

Im PHP2Shell-Artikel habe ich geschrieben, warum statische Seiten in so einer Woche gelassen bleiben: Bei ihrer Auslieferung läuft kein PHP. Das stimmt, aber es ist nur die halbe Wahrheit. Gebaut werden diese Seiten — auch diese Knowledge Base — mit Node.js. Node2Shell ist deshalb ein guter Anlass für eine Frage, die ich in Node.js und npm absichern bewusst ausgeklammert habe: Wie kommt Node.js eigentlich auf die Build-Maschine, und wie schnell kommt ein Sicherheitsfix hinterher?

Der Artikel hat drei Teile: zuerst kurz, was hinter Node2Shell steckt und was es mit PHP2Shell verbindet. Dann, was das für Projekte heißt, die statische Websites mit Node.js erzeugen. Und dann, als größter Teil, alle gängigen Wege, Node.js zu installieren, zu verwalten und zu aktualisieren — mit Kurzanleitung, Tipps und den Stellen, an denen es hakt. Einige Befehle und Zahlen darin habe ich selbst in frischen Containern nachgemessen.

Die Kurzfassung

Was Stand
Lücke CVE-2026-111497, Out-of-bounds-Write im Brotli-Decoder von Google, Versionen 1.0.3 bis 1.2.0
Schwere CVSS 4.0: 9,2 (kritisch) im CVE-Eintrag; Node.js selbst stuft „high" ein
Behoben in Node.js 22.23.4, 24.22.1, 26.12.1 (21. Oktober 2026); Brotli 1.0.10, 1.1.1, 1.2.1; Bun 1.4.4
Angriffsweg Node entpackt angreifergesteuerte Brotli-Daten — vor allem Antworten auf fetch(), eingehend mit passender Middleware, außerdem jeder Code, der fremde Brotli-Daten entpackt
Ausnutzung Codeausführung von Imperva im Labor gezeigt, Exploit nicht veröffentlicht; ein öffentlicher Proof-of-Concept bringt Standard-Builds zum Absturz und liest Speicher aus; keine Angriffe in freier Wildbahn bekannt
Workaround nur als Notbehelf: Brotli aus fremden Quellen nicht entpacken; die Lösung ist das Update der Runtime
Für statische Sites Auslieferung unberührt; der Build ist betroffen, sobald er Daten von fremden Servern holt
Mein Weg NodeSource-Paketquellen unter Debian und Ubuntu, Updates über apt

Was hinter Node2Shell steckt

Brotli ist ein Kompressionsverfahren von Google, das Webserver und Browser für HTTP-Antworten nutzen (Content-Encoding: br). Node.js bringt eine eigene Kopie des Brotli-Decoders mit. Ein Brotli-Datenstrom ist in Blöcke unterteilt, zwischen deren Typen der Decoder umschaltet. In den Brotli-Versionen 1.0.3 bis 1.2.0 konnte die zuständige Funktion einen unmöglichen Blockwechsel nicht als Fehler melden: Der Streaming-Pfad las ihn als „brauche mehr Eingabe", ein schneller Pfad ignorierte den Rückgabewert ganz. So schrieb der Decoder weitere Bytes des Angreifers über das Ende eines internen Puffers hinaus. Laut der Sicherheitsmeldung von Node.js (öffnet in neuem Tab) (englisch) kann das zum Absturz führen, Prozessspeicher preisgeben oder Code ausführen.

Pikant ist die Vorgeschichte: Die Korrektur steckt schon seit dem 27. Februar 2026 im Brotli-Quellcode (öffnet in neuem Tab) (englisch), damals mit der unauffälligen Commit-Nachricht „Detect and report invalid stream earlier." Öffentlich als Sicherheitsproblem bekannt und in Releases gegossen wurde sie erst am 21. Oktober. Gefunden haben den Fehler laut CVE-Eintrag Khai Tran und Sylvie Mayer von OpenAI Codex Security; laut OpenAI schon im Februar, ohne dass damals klar war, dass der Fix eine Sicherheitslücke schließt.

Der Name „Node2Shell" stammt nicht von Node.js. Das Red Team von Imperva (öffnet in neuem Tab) (englisch) nennt seine Analyse so und hat damit Codeausführung gegen ein unverändertes offizielles Node.js-24.22.0-Image gezeigt, den Exploit aber nicht veröffentlicht. Weil der Fehler in Brotli selbst liegt und weit über Node hinausreicht, kursieren auch „HTTP2Shell" und „Broccoli2Shell". Seit dem 22. Oktober gibt es außerdem einen öffentlichen Proof-of-Concept; gegen Standard-Builds bringt er Node aber nur zum Absturz und liest begrenzt Speicher aus, Code ausgeführt hat er nur auf einem absichtlich geschwächten Test-Build. Angriffe in freier Wildbahn waren bis zum 22. Oktober laut The Hacker News (öffnet in neuem Tab) (englisch) nicht bekannt, und die CISA hat die Lücke nicht in ihren Katalog aktiv ausgenutzter Schwachstellen aufgenommen.

Wo dekomprimiert Node Brotli? Die Meldung nennt das Modul node:zlib. Entscheidend ist aber, wer es aufruft:

  • fetch() dekomprimiert br automatisch. Antwortet ein Server mit Content-Encoding: br, entpackt Node die Antwort, ohne dass dein Code etwas davon merkt. Ein bösartiger oder kompromittierter Server kann so einen ungepatchten Node-Prozess angreifen, der ihn abfragt. br aus dem Accept-Encoding-Header zu streichen, hilft nicht zuverlässig, denn ein Angreifer schickt Brotli auch ungefragt.
  • Ein reiner node:http-Server dekomprimiert eingehende Daten nicht. Eingehend wird es erst mit Middleware gefährlich, die Request-Bodys mit Content-Encoding: br entpackt; Imperva und Aikido nennen Express 5 und body-parser 2.
  • Andere Laufzeiten und Sprachen: Bun ist mit 1.4.4 gefixt. Betroffen sind außerdem unter anderem die Python-Pakete Brotli und brotlicffi, Envoy und Istio, wenn dort Brotli-Dekompression eingeschaltet ist, sowie curl über seine Brotli-Bibliothek, wenn du mit --compressed abrufst. Die Liste bei Aikido ist lang.
  • Nicht nur HTTP: Laut Patch the Planet ist jeder Code betroffen, der fremde Brotli-Daten entpackt, auch Werkzeuge für WOFF2-Schriften und Datendateien.

Eine Falle beim Prüfen: node -p process.versions.brotli zeigt auch in der gefixten Version 24.22.1 noch 1.2.0 (Node 22 bleibt bei 1.1.0), denn Node hat den Fix als einzelnen Commit übernommen, nicht die neue Brotli-Version. Ich habe das in einem frischen Container nachgesehen. Maßgeblich ist allein die Node-Version: node -v muss mindestens v22.23.4, v24.22.1 oder v26.12.1 zeigen.

Die Parallele zu PHP2Shell — und der Unterschied

Die beiden Lücken haben mehr gemeinsam als den Tag ihrer Veröffentlichung. Beide stehen auf Patch the Planet (öffnet in neuem Tab) (englisch), einer Initiative von OpenAI, die mit KI und Sicherheitsfachleuten wie Trail of Bits und Calif Befunde in Open-Source-Projekten prüft, Patches entwickelt und die Veröffentlichung mit den Maintainern koordiniert. Beide liegen eine Ebene unter der Anwendung, sodass kein Update von WordPress, Express oder einem Plugin hilft, sondern nur eines der Laufzeitumgebung. Und bei beiden lag die Wurzel lange herum, bevor jemand sie als Sicherheitsproblem erkannt hat — bei PHP seit einem Audit 2024, bei Brotli seit dem stillen Fix im Februar.

Der Unterschied liegt in der Angriffsfläche. PHP2Shell steckte in jedem PHP-Server und ließ sich über eine präparierte Anfrage auslösen, noch bevor Anwendungscode lief, am gefährlichsten mit Apache und mod_php. Node2Shell braucht einen Brotli-Decoder auf dem Datenweg, und der liegt vor allem auf der ausgehenden Seite: bei jedem Node-Prozess, der Daten von einem fremden Server holt. Das trifft Crawler, Webhook-Empfänger, Link-Vorschauen — und Build-Prozesse. Eine Web Application Firewall vor deinem Server sieht diese Antworten nie. Als Notbehelf bleibt, fremde Brotli-Daten gar nicht erst zu entpacken: eingehend Anfragen mit Content-Encoding: br vor dem Body-Parser abweisen, ausgehend betroffene fetch()-Abrufe aussetzen. Weil sich fetch() die automatische Dekompression nicht abgewöhnen lässt, ist das Update die eigentliche Lösung.

Was das für statische Websites heißt

Für die Auslieferung ändert sich nichts: Eine fertige HTML-Datei auf einem Apache oder nginx dekomprimiert keine Brotli-Daten von Angreifern. Interessant wird es beim Bauen.

Ein Generator wie Eleventy, Astro oder Hugo mit Node-Werkzeugen ist für die Dauer des Builds ein Node-Prozess, der Code ausführt und manchmal Daten aus dem Netz holt. Betroffen ist er, sobald er Brotli-Daten aus fremden Quellen entpackt, und das passiert vor allem bei fetch() gegen fremde Server:

  • Headless-CMS und APIs, deren Inhalte in den Build fließen.
  • Remote-Daten in _data-Dateien, etwa ein JSON von einem Dienst.
  • @11ty/eleventy-fetch (öffnet in neuem Tab) und Remote-Bilder mit @11ty/eleventy-img. Seit Version 5 nutzt eleventy-fetch das globale fetch() von Node, ältere Versionen nutzen node-fetch. Beide entpacken eine Brotli-Antwort automatisch über node:zlib, betroffen ist also jede Version.
  • Build-Skripte, die Feeds, Statistiken oder Vorschaubilder fremder Seiten abholen.
  • Werkzeuge für Schriften und Datendateien, die Brotli-komprimierte Dateien aus fremden Quellen entpacken, etwa WOFF2-Schriften.

Weniger kritisch ist npm selbst. In meiner Messung schickt npm 11 der Registry den Header Accept-Encoding: gzip,deflate und bekommt gzip zurück; Brotli kommt im Normalbetrieb also nicht an. Ganz ausschließen würde ich es nicht: Die Registry liefert Brotli, wenn ein Client es anbietet, und npm entpackt Brotli auch dann, wenn es nicht danach gefragt hat. Ein kompromittierter Mirror oder Proxy dazwischen kommt so trotzdem an den Decoder.

Das Risiko ist überschaubar, wenn du nur deinen eigenen Quellen vertraust. Ein Angreifer müsste einen Server kontrollieren, den dein Build abfragt. Aber genau das passiert bei Supply-Chain-Angriffen ständig: übernommene Domains, kompromittierte CMS-Instanzen, gekaperte CDNs. Und eine Build-Maschine ist ein lohnendes Ziel, weil dort Zugangsdaten für Deployments, Repositories und Server liegen.

Die eigentliche Lehre aus Node2Shell ist für mich deshalb eine andere: Wie schnell kommt der Fix auf deine Build-Maschine — und weißt du das überhaupt? Am 21. Oktober sah das je nach Installationsweg sehr unterschiedlich aus:

Weg Gefixte Version verfügbar (21. Oktober, UTC, amd64)
nodejs.org, Release auf GitHub ~10:38
Snap (node, Kanal 24) 12:30
Docker, node:24 und node:24-slim ~14:40
Docker, node:lts spätestens 15:56
NodeSource (node_24.x) 16:17
Debian 12 und 13 (über die System-Bibliothek libbrotli) 23. Oktober
Ubuntu (über libbrotli) Stand dieses Artikels noch offen
Versionsmanager (nvm, fnm, n, mise) sofort, sobald du sie aufrufst
DDEV mit Standard-Einstellung erst mit dem nächsten DDEV-Release

Die Zeiten für NodeSource, Snap und Docker habe ich aus den Zeitstempeln der Paketquellen und Registries abgelesen, bei NodeSource ist es der Zeitstempel des Pakets. Für andere Architekturen kamen manche Builds später, bei Docker für ppc64le erst in der Nacht. Die Tabelle zeigt schon, worum es im Rest geht: Jeder Weg hat eine eigene Verzögerung und einen eigenen Mechanismus, mit dem das Update bei dir ankommt — oder eben nicht.

Wie du Node.js installierst: die Wege im Überblick

Bevor es ins Detail geht, ein Blick auf den Release-Plan, denn er bestimmt, welche Version du überhaupt willst. Bis Node.js 26 erschienen zwei Hauptversionen im Jahr, und die geraden wurden zu Long-Term-Support-Versionen (LTS). Laut Release-Plan (öffnet in neuem Tab) (englisch) ist Node.js 24 seit dem 20. Oktober in der Maintenance-Phase und bekommt Sicherheitsupdates bis April 2028. Node.js 26 wird am 28. Oktober zur aktiven LTS-Version. Node.js 22 läuft bis April 2027, Node.js 20 ist seit April 2026 am Ende. Ab Node.js 27 ändert sich das Modell (öffnet in neuem Tab) (englisch): nur noch eine Hauptversion pro Jahr, und jede wird LTS. Zum Testen kommender Änderungen gibt es dann statt ungerader Versionen einen Alpha-Kanal.

Für statische Websites heißt das: Nimm eine LTS-Version, also heute 24, in den nächsten Wochen 26. Current- und Alpha-Versionen sind für Experimente, nicht für Builds.

Weg Gut für Updates kommen über Mehrere Versionen
NodeSource Server und Arbeitsrechner unter Debian/Ubuntu apt, auch automatisch nein, eine Hauptversion pro System
Distribution Systemwerkzeuge, die Node nur nebenbei brauchen Sicherheitsupdates der Distribution nein
Offizielle Binaries Sonderfälle, Systeme ohne Paketquelle Handarbeit ja, manuell
Snap Ubuntu-Desktops automatischer Snap-Refresh über Kanäle
Versionsmanager Entwicklungsrechner mit mehreren Projekten eigener Befehl, pro Benutzer ja
Container und CI reproduzierbare Builds neues Image oder Action-Lauf ja, pro Image

Mein Favorit: die Paketquellen von NodeSource

Auf Debian- und Ubuntu-Systemen installiere ich Node.js am liebsten aus den Paketquellen von NodeSource (öffnet in neuem Tab) (englisch). Der Grund ist einfach: Ich bekomme die aktuelle LTS-Version als normales .deb-Paket, und sie wird wie jedes andere Paket mit apt aktualisiert — auf Wunsch automatisch. Pro Hauptversion gibt es ein eigenes Repository (node_22.x, node_24.x, node_26.x), sodass ein Update nie unbemerkt die Hauptversion wechselt. Das Paket nodejs bringt npm und npx gleich mit, in Node 24 außerdem noch Corepack.

Kurzanleitung mit dem Setup-Skript

NodeSource bietet ein Einrichtungsskript an. Es ist rund 120 Zeilen lang und gut lesbar — schau es dir an, bevor du es ausführst:

curl -fsSL -o nodesource_setup.sh https://deb.nodesource.com/setup_24.x
less nodesource_setup.sh
sudo bash nodesource_setup.sh
sudo apt install nodejs
node -v && npm -v

Das Skript installiert ein paar Voraussetzungen, legt den Signaturschlüssel unter /usr/share/keyrings/nodesource.gpg ab, schreibt die Paketquelle im neuen deb822-Format nach /etc/apt/sources.list.d/nodesource.sources und legt in /etc/apt/preferences.d/nodejs ein Pinning mit Priorität 600 an. Das sorgt dafür, dass apt das Paket von NodeSource dem nodejs der Distribution vorzieht. Neben setup_24.x gibt es setup_22.x, setup_26.x sowie setup_lts.x und setup_current.x, die jeweils auf die aktuelle LTS- bzw. Current-Linie zeigen.

Kurzanleitung ohne Skript

Wenn du lieber jeden Schritt selbst machst, geht es auch von Hand:

Terminal
sudo apt install -y ca-certificates curl gnupg
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
  | sudo gpg --dearmor -o /usr/share/keyrings/nodesource.gpg

sudo tee /etc/apt/sources.list.d/nodesource.sources > /dev/null <<'EOF'
Types: deb
URIs: https://deb.nodesource.com/node_24.x
Suites: nodistro
Components: main
Architectures: amd64
Signed-By: /usr/share/keyrings/nodesource.gpg
EOF

sudo tee /etc/apt/preferences.d/nodejs > /dev/null <<'EOF'
Package: nodejs
Pin: origin deb.nodesource.com
Pin-Priority: 600
EOF

sudo apt update && sudo apt install -y nodejs

Auf ARM-Rechnern schreibst du arm64 statt amd64. Den Schlüssel kannst du vorher prüfen: gpg --show-keys --with-fingerprint /usr/share/keyrings/nodesource.gpg muss den Fingerabdruck 6F71 F525 2828 41EE DAF8 51B4 2F59 B5F9 9B1B E0B4 zeigen.

Updates automatisch einspielen

Hier liegt für mich der größte Vorteil. Mit unattended-upgrades spielt das System Sicherheitsupdates von selbst ein — standardmäßig aber nur aus den Quellen der Distribution. NodeSource musst du ausdrücklich freigeben. Der übliche Weg über Origin und Label funktioniert hier nicht gut, denn die Release-Datei von NodeSource trägt dort nur ein . nodistro. Sauberer ist es, die Quelle über ihren Host freizugeben:

echo 'Unattended-Upgrade::Origins-Pattern { "site=deb.nodesource.com"; };' \
  | sudo tee /etc/apt/apt.conf.d/52nodesource-unattended
sudo unattended-upgrade --dry-run --debug 2>&1 | grep -i nodejs

Ich habe das in einem frischen Debian-13-Container getestet: mit 24.22.0 installiert, erkannte unattended-upgrade das Paket 24.22.1 von NodeSource als Update und lud es herunter. Die Zeile ergänzt die Standardquellen, sie ersetzt sie nicht.

Ein Punkt gilt für jeden Installationsweg: Ein laufender Node-Prozess nutzt den alten Code im Speicher weiter. Dienste, die dauerhaft laufen — ein Node-Server, ein Worker, ein Build-Daemon —, musst du nach dem Update neu starten. Unter Debian und Ubuntu zeigt sudo needrestart -r l (Paket needrestart, unter Debian meist erst nachzuinstallieren), welche Prozesse noch alten Code nutzen. Für einen statischen Build, der bei jedem Lauf neu startet, ist das kein Thema.

Tipps und Stolperstellen

  • Hauptversion wechseln: Das Setup-Skript der neuen Linie ausführen (etwa setup_26.x), dann sudo apt install nodejs. Es räumt die alte Quelle vorher selbst weg. Von Hand änderst du die URIs-Zeile auf node_26.x.
  • Bestimmte Version installieren: apt-cache policy nodejs zeigt alle verfügbaren Versionen, sudo apt install nodejs=24.22.1-1nodesource1 installiert genau eine.
  • Nicht npm aus der Distribution dazu installieren. Das NodeSource-Paket enthält npm bereits und steht mit dem Distributionspaket npm in Konflikt. Manche ältere Anleitung sagt apt install nodejs npm — das ist für NodeSource falsch.
  • apt-mark hold nodejs ist meist die falsche Bremse. Es friert auch Sicherheitsupdates ein. Weil jedes NodeSource-Repository nur eine Hauptversion enthält, brauchst du keine zusätzliche Sperre gegen große Sprünge.
  • Architekturen: Das Skript unterstützt nur amd64 und arm64. Für einen älteren Raspberry Pi mit 32-Bit-System brauchst du einen anderen Weg.
  • N|Solid liegt im selben Repository. Das ist NodeSources kommerzielle Laufzeit mit Monitoring; für normale Projekte installierst du nur nodejs.
  • Sauber entfernen: sudo apt purge nodejs, dann /etc/apt/sources.list.d/nodesource.sources, /usr/share/keyrings/nodesource.gpg und die Datei nodejs in /etc/apt/preferences.d/ löschen, beim Skriptweg auch nsolid. Die Anleitung im NodeSource-Repository nennt noch die alten Pfade aus der Zeit vor deb822.
  • Für RPM-Systeme gibt es dasselbe unter rpm.nodesource.com, mit setup_24.x für Fedora, RHEL und Verwandte.

Die Pakete der Distribution

Der naheliegendste Weg ist sudo apt install nodejs aus den Paketquellen von Debian oder Ubuntu. Das Problem ist das Alter:

Distribution nodejs im Standard-Repository Status der Node-Linie bei Node.js
Debian 12 „bookworm" 18.20.4 Ende seit April 2025
Debian 13 „trixie" 20.19.2 Ende seit April 2026
Ubuntu 24.04 LTS 18.19.1 Ende seit April 2025
Ubuntu 26.04 LTS 22.22.1 Maintenance bis April 2027

Das heißt nicht, dass diese Pakete unsicher wären. Debian und Ubuntu pflegen Sicherheitskorrekturen für ihre Versionen selbst ein, auch über das offizielle Ende einer Node-Linie hinaus. Bei Node2Shell zeigt sich, wie das funktioniert: Debian baut Node gegen die Brotli-Bibliothek des Systems statt gegen die mitgelieferte Kopie. Der Fix kam deshalb nicht als neues Node-Paket, sondern über libbrotli1 — mit DSA-6582-1 (öffnet in neuem Tab) (englisch) am 23. Oktober für Debian 13 und einem LTS-Update für Debian 12. Ubuntu stuft sein nodejs ab 22.04 aus demselben Grund als nicht betroffen ein (für 20.04 steht es auf „needed"); das Update für die Brotli-Bibliothek stand bei Redaktionsschluss aber noch aus.

Für die Arbeit an aktuellen Webprojekten passen diese Versionen trotzdem selten. Mit Node 18 auf Debian 12 und Ubuntu 24.04 hast du eine Linie, die viele Werkzeuge nicht mehr unterstützen, und npm ist bei Debian ein eigenes, deutlich älteres Paket (in Debian 13 npm 9.2). Vor allem aber willst du lokal dieselbe LTS-Version wie in CI und im Container, und die bestimmt bei den Distributionspaketen die Distribution, nicht du. Gut sind sie für Systemwerkzeuge, die Node nebenbei brauchen.

Die offiziellen Binaries von nodejs.org

Auf nodejs.org (öffnet in neuem Tab) (englisch) gibt es fertige Archive für Linux, macOS und Windows. Das ist der Weg für Systeme ohne passende Paketquelle oder wenn du eine ganz bestimmte Version brauchst. Mit Prüfung von Prüfsumme und Signatur sieht das so aus:

Terminal
VER=v24.22.1
curl -fsSLO "https://nodejs.org/dist/$VER/node-$VER-linux-x64.tar.xz"
curl -fsSLO "https://nodejs.org/dist/$VER/SHASUMS256.txt.asc"
curl -fsSLo nodejs-keyring.kbx https://github.com/nodejs/release-keys/raw/HEAD/gpg/pubring.kbx

# Verify the signature, then the checksum of the archive
gpgv --keyring=./nodejs-keyring.kbx --output SHASUMS256.txt < SHASUMS256.txt.asc
sha256sum --check --ignore-missing SHASUMS256.txt

# Install into its own directory and link it
sudo mkdir -p /opt/node
sudo tar -xJf "node-$VER-linux-x64.tar.xz" -C /opt/node
sudo ln -sfn "/opt/node/node-$VER-linux-x64" /opt/node/current
echo 'export PATH="/opt/node/current/bin:$PATH"' >> ~/.zshrc

Der Haken ist offensichtlich: Updates passieren nur, wenn du sie machst. Das ist für einen Server, der Node dauerhaft braucht, der schlechteste der Wege — es sei denn, du automatisierst ihn selbst.

Snap

Unter Ubuntu ist Snap eine bequeme Alternative. Das Paket node stammt von der OpenJS Foundation, also aus dem Umfeld des Node-Projekts selbst, und war am 21. Oktober als einer der ersten Wege gefixt:

sudo snap install node --classic --channel=24/stable
node -v
# Later, switch the major version:
sudo snap refresh node --channel=26/stable

Snaps aktualisieren sich automatisch innerhalb ihres Kanals, das Sicherheitsupdate kam also ohne dein Zutun. Dafür nimmst du die Eigenheiten von Snap in Kauf: --classic hebt die übliche Abschottung auf, die Pfade weichen ab (/snap/bin), und auf Servern ohne Snap-Unterstützung fällt der Weg aus.

Versionsmanager: nvm, fnm, n, mise und Co.

Auf Entwicklungsrechnern mit mehreren Projekten sind Versionsmanager der übliche Weg. Sie installieren Node.js pro Benutzer, oft mehrere Versionen nebeneinander, und wechseln je nach Projekt. Die Seite nodejs.org/download (öffnet in neuem Tab) (englisch) empfiehlt für Linux und macOS selbst nvm und fnm, daneben Docker. Ich habe mir die verbreiteten Werkzeuge genauer angesehen und zwei Dinge gemessen, die selten in Vergleichen stehen: ob sie die heruntergeladenen Node-Archive prüfen, und was sie beim Start einer neuen zsh kosten.

Werkzeug Geschrieben in Prüft Downloads Shell-Start (gemessen) Globale Pakete bei Patch-Update
nvm (öffnet in neuem Tab) 0.40.8 Shell SHA-256 +0,5 bis 1 s, mit --no-use gut +100 ms gehen verloren, Übernahme mit --reinstall-packages-from
fnm (öffnet in neuem Tab) 1.39.0 Rust nein, nur HTTPS kaum messbar (< 15 ms) gehen verloren
n (öffnet in neuem Tab) 11.0.0 Bash nein, nur HTTPS kein Shell-Hook bleiben erhalten
mise (öffnet in neuem Tab) 2026.10 Rust SHA-256 und Signatur einige Dutzend ms gehen verloren, Werkzeuge stattdessen über mise
asdf 0.20 / nodenv 1.6 (mit node-build) Go / Bash SHA-256 aus fester Liste — je Version getrennt

Die Startzeiten habe ich in einem frischen Debian-13-Container gemessen, als Mittelwert aus 20 Starts von zsh -i -c exit gegenüber einer zsh ohne Versionsmanager (8 bis 16 ms). Die absoluten Zahlen hängen stark vom Rechner und seiner Last ab: In einer unabhängigen Wiederholung lag nvm bei 0,7 bis 1 Sekunde, die Reihenfolge blieb in jedem Lauf gleich. Die Prüfung der Downloads habe ich im Quellcode nachgesehen. Dass fnm und n den Archiven nur über HTTPS vertrauen, ist kein Drama — aber nvm und mise zeigen, dass es besser geht, und mise prüft als einziges auch die Signatur der Prüfsummen.

Volta fehlt in der Tabelle mit Absicht: Das README sagt seit 2025 „Volta is unmaintained" und empfiehlt den Umstieg auf mise.

Kurzanleitungen

nvm ist der Klassiker, pro Benutzer, in reinem Shell-Code:

curl -fsSL -o install-nvm.sh https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh
less install-nvm.sh && bash install-nvm.sh   # adds the loader to ~/.zshrc
nvm install 24
# Security update within the 24 line, keeping global packages:
nvm install 24 --reinstall-packages-from=current

Den automatischen Wechsel per .nvmrc beim Betreten eines Verzeichnisses macht nvm nicht von selbst; das README liefert dafür einen eigenen zsh-Hook mit add-zsh-hook chpwd. Und die halbe Sekunde Startzeit ist real. Wenn sie dich stört, hast du zwei Wege: --no-use hinter dem nvm.sh-Aufruf spart das automatische Aktivieren, dann musst du aber in jeder neuen Shell selbst nvm use aufrufen, bevor node da ist. Das nvm-Plugin von Oh My Zsh lädt nvm mit zstyle ':omz:plugins:nvm' lazy yes erst, wenn du nvm, node oder npm aufrufst.

fnm ist schnell und kann .nvmrc und .node-version direkt lesen:

curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell
# in ~/.zshrc:
eval "$(fnm env --use-on-cd --shell zsh)"
fnm install 24 && fnm default 24   # 24 resolves to the newest 24.x

n hat keinen Shell-Hook, sondern ersetzt die eine Node-Version unter /usr/local (oder einem eigenen N_PREFIX). Das macht es einfach und für alle Benutzer und Dienste sichtbar — DDEV nutzt es intern:

npm install -g n      # or bootstrap without npm, see the README
sudo n lts            # or: sudo n 24

mise verwaltet neben Node auch Python, Go und viele andere Werkzeuge und ist der Nachfolger, den Volta selbst empfiehlt:

curl -fsSL https://mise.run | sh   # or via extrepo/apt, see the docs
# in ~/.zshrc:
eval "$(~/.local/bin/mise activate zsh)"
mise use -g node@24
mise upgrade node                  # newest 24.x

Eine Eigenheit: mise liest .nvmrc und .node-version erst, wenn du es mit mise settings add idiomatic_version_file_enable_tools node erlaubst. Ohne das ignoriert es die Dateien stillschweigend.

Die typische Falle der Versionsmanager mit Shell-Hook

nvm, fnm und mise hängen sich in deine interaktive Shell; n nicht, es installiert nach /usr/local. Ein systemd-Dienst, ein Cronjob oder ein sudo-Aufruf sieht diese Shell nicht — und findet entweder gar kein Node oder das alte aus der Distribution. Für dauerhaft laufende Dienste auf einem Server ist ein Versionsmanager deshalb der falsche Weg. Wenn es doch sein muss, nutzt du die Shims von mise oder mise exec, deren Pfad auch nach Updates gleich bleibt. Bei nvm trägst du den absoluten Pfad ein (nvm which 24 verrät ihn) und denkst daran, dass er sich mit jedem Update ändert.

Container, DDEV und CI

Docker. Die offiziellen Images (öffnet in neuem Tab) (englisch) gibt es in den Varianten Standard, slim und alpine. Ein paar Dinge, die du wissen solltest:

  • node:24 basiert derzeit auf Debian 12 „bookworm", node:26 auf Debian 13 „trixie". Willst du Node 24 auf trixie, schreibst du node:24-trixie-slim.
  • node:lts folgt laut Image-Doku der aktiven LTS-Version. In dieser Woche zeigt der Tag noch auf 24, das schon in der Maintenance-Phase ist. Wenn Node 26 am 28. Oktober LTS wird, dürfte er auf 26 springen — mit neuer Hauptversion und neuer Debian-Basis. Für Builds pinne ich deshalb die Hauptversion, etwa FROM node:24-bookworm-slim.
  • Die Alpine-Varianten mit musl gelten beim Node-Projekt als experimentell. Wenn du keinen guten Grund hast, nimm slim.
  • Ein Digest-Pin (node:24-bookworm-slim@sha256:…) macht Builds exakt reproduzierbar, verhindert aber auch Sicherheitsupdates, solange kein Werkzeug wie Renovate oder Dependabot ihn aktualisiert.
  • Nach einem Sicherheitsrelease reicht ein Neustart nicht: docker build --pull holt das neue Basis-Image.

DDEV. Wenn du deine Projekte in DDEV (öffnet in neuem Tab) (englisch) baust, steckt hier die größte Überraschung. DDEV installiert Node im Web-Container mit n, und die Einstellung nodejs_version entscheidet, was passiert. Der Standardwert 24 nutzt die Version, die im Image der installierten DDEV-Version vorinstalliert ist — bei DDEV 1.25.4 vom September also zwangsläufig eine ältere als 24.22.1. Am einfachsten bekommst du den Fix so; eine exakte Version wie "24.22.1" geht auch, muss dann aber bei jedem Release nachgezogen werden:

.ddev/config.yaml
nodejs_version: "v24"   # newest 24.x at the time the image is built

Danach baust du das Image neu: ddev restart --no-cache oder ddev utility rebuild. Die Version bleibt bis zum nächsten Neubau fest; nach dem nächsten Node-Sicherheitsrelease wiederholst du den Neubau. Mit nodejs_version: "auto" liest DDEV die Version aus .nvmrc, .node-version oder engines.node deines Projekts.

GitHub Actions. actions/setup-node (öffnet in neuem Tab) (englisch) liest die Version mit node-version-file aus .nvmrc, .node-version, .tool-versions, mise.toml oder package.json. Mit check-latest: true holt die Action die neueste passende Version, statt die auf dem Runner zwischengespeicherte zu nehmen — direkt nach einem Sicherheitsrelease ist das der Unterschied.

macOS und Windows in aller Kürze: Unter macOS installiert Homebrew mit brew install node die Current-Version; die LTS-Version bekommst du mit brew install node@24, die du selbst in den PATH legen musst. Unter Windows gibt es winget install OpenJS.NodeJS.LTS und den offiziellen Installer.

Die Node-Version im Projekt festhalten

Egal wie Node installiert ist: Das Projekt sollte sagen, welche Version es braucht. Dafür gibt es mehrere Dateien, und nicht jedes Werkzeug liest jede:

Datei gelesen von
.nvmrc nvm, fnm, n, setup-node, DDEV (auto); asdf und mise nach Freigabe
.node-version fnm, n, nodenv, setup-node, DDEV (auto); asdf und mise nach Freigabe
.tool-versions / mise.toml asdf bzw. mise, setup-node
engines.node in package.json npm (nur als Warnung), fnm, n, setup-node, DDEV
devEngines.runtime in package.json npm (bricht standardmäßig ab), n 11, setup-node; mise nach Freigabe

Zwei Hinweise dazu. engines ist für dein eigenes Projekt nur eine Empfehlung: npm warnt, installiert aber trotzdem, solange engine-strict nicht gesetzt ist. Das neuere Feld devEngines (öffnet in neuem Tab) (englisch) ist genau für Leute gedacht, die am Quellcode arbeiten, und npm bricht bei einer falschen Version standardmäßig ab:

package.json
{
  "devEngines": {
    "runtime": { "name": "node", "version": ">=24.22.1" }
  }
}

Mit der Patch-Version als Untergrenze verhindert das Feld nach einem Sicherheitsrelease, dass jemand versehentlich mit der verwundbaren Version baut. Eine .nvmrc mit 24 daneben schadet nicht und bedient die Werkzeuge, die devEngines noch nicht kennen.

Von Sicherheitsreleases rechtzeitig erfahren

Das beste Update-Verfahren nützt wenig, wenn du nicht weißt, dass es etwas zu tun gibt. Das Node-Projekt bietet dafür:

  • den Feed nodejs.org/en/feed/vulnerability.xml (öffnet in neuem Tab) (englisch) für jeden Feed-Reader,
  • die Mailingliste nodejs-sec (öffnet in neuem Tab) (englisch), die nur Ankündigungen verschickt und wenig Verkehr hat,
  • und das Werkzeug is-my-node-vulnerable aus der Node.js-Organisation, das deine laufende Version gegen die Sicherheitsdatenbank prüft: npx is-my-node-vulnerable. Es lädt die Daten bei jedem Lauf frisch und eignet sich gut als Schritt in einer CI-Pipeline. Ich rufe es — wie jedes npm-Werkzeug — über Socket Firewall auf.

Tipps aus der Praxis

  • Eine Installationsart pro System. Node aus NodeSource und zusätzlich nvm im Benutzerkonto führt früher oder später zu der Frage, welches node gerade läuft. command -v node und type -a node zeigen es.
  • Nach einem Sicherheitsrelease alle Orte durchgehen: Arbeitsrechner, Build-Server, CI, Container-Basis-Images, DDEV-Projekte. Die Tabelle oben zeigt, dass jeder Ort seinen eigenen Takt hat.
  • node -v prüfen, nicht process.versions.brotli. Der Fix kam bei Node 24 als einzelner Commit, die Brotli-Versionsangabe blieb gleich.
  • Lange laufende Node-Prozesse neu starten. Ein Update auf der Platte nützt dem Prozess im Speicher nichts.
  • Build-Abhängigkeiten von fremden Servern bewusst halten. Jedes fetch() im Build ist ein Server, dem du vertraust. Wo es geht, cache Remote-Daten und hol sie nicht bei jedem Build neu.

FAQ

Ist meine statische Website von Node2Shell betroffen?

Die ausgelieferten Seiten nicht, denn dort läuft kein Node. Der Build ist betroffen, wenn er mit einer verwundbaren Node-Version Daten von fremden Servern per fetch() holt oder fremde Brotli-Daten anders entpackt, etwa WOFF2-Schriften. Aktualisier Node auf deiner Build-Maschine und in CI auf 22.23.4, 24.22.1 oder 26.12.1.

Hilft es, br aus Accept-Encoding zu entfernen?

Nicht zuverlässig. Ein bösartiger Server kann Brotli auch schicken, wenn der Client es nicht angeboten hat, und fetch() dekomprimiert dann trotzdem. Node.js selbst nennt keinen Workaround, Patch the Planet rät bis zum Update, Brotli aus fremden Quellen gar nicht zu entpacken. Die Lösung ist das Update.

Ich nutze die Node-Version meiner Distribution. Muss ich etwas tun?

Ja, aber anders: Bei Debian kommt der Fix über das Paket libbrotli1, das ein normales sudo apt upgrade mitbringt. Danach müssen laufende Node-Prozesse neu starten. Bei Ubuntu lohnt ein Blick auf die Sicherheitsseite zur CVE (öffnet in neuem Tab) (englisch).

NodeSource, nvm oder Docker — was ist am sichersten?

Keiner der Wege ist an sich sicherer. Sicher ist der Weg, über den der Fix bei dir zuverlässig und schnell ankommt. Auf Servern spricht viel für einen Paketmanager mit automatischen Updates, auf Entwicklungsrechnern für einen Versionsmanager, in CI für gepinnte Images mit einem Bot, der sie aktuell hält.

Warum zeigt process.versions.brotli nach dem Update noch die alte Brotli-Version?

Weil Node den Fix als einzelnen Commit in seine mitgelieferte Brotli-Kopie übernommen hat, ohne die Versionsangabe zu ändern: Node 24 und 26 zeigen weiter 1.2.0, Node 22 1.1.0. Prüf stattdessen node -v.

Ist Corepack noch dabei?

In Node.js 24 ja, als experimentelle Funktion. Ab Node.js 25 liefert das Projekt Corepack nicht mehr mit; wenn du es brauchst, installierst du es mit npm install -g corepack, und Werkzeuge wie mise übernehmen die Aufgabe selbst.

Fazit

Node2Shell ist keine zweite PHP2Shell. Die Lücke ist ernst, aber sie trifft vor allem Node-Prozesse, die Daten von fremden Servern entpacken — und damit auch Builds, die Remote-Daten holen. Die Antwort ist dieselbe wie bei PHP: die Laufzeit aktualisieren, weil kein Anwendungs-Update hilft.

Welcher Installationsweg der richtige ist, hängt davon ab, wo Node läuft. Auf Debian- und Ubuntu-Servern sind die Paketquellen von NodeSource für mich die beste Mischung aus aktueller Version und Pflegeaufwand, weil Updates über apt kommen und sich automatisieren lassen. Die Pakete der Distribution sind gut gepflegt, aber für Webprojekte zu alt. Snap ist schnell und automatisch, wenn du Snap ohnehin nutzt. Versionsmanager gehören auf Entwicklungsrechner — mit mise als gründlichstem Prüfer, fnm als schnellstem und nvm als bekanntestem, aber trägem. Container und CI brauchen gepinnte Hauptversionen und jemanden, der die Images nach einem Sicherheitsrelease neu baut. Und DDEV braucht nach so einem Release einen bewussten Neubau.

Entscheidend ist am Ende nicht der Weg, sondern dass du für jeden Ort, an dem bei dir Node läuft, die Antwort auf eine Frage kennst: Wie kommt der nächste Fix hierher, und wie lange dauert das?

Weiterlesen

Wie es bei PHP2Shell aussah und warum statische Seiten bei der Auslieferung gelassen bleiben, steht in PHP2Shell. Die andere Hälfte der Absicherung, also npm, Lockfiles und Install-Skripte, beschreibt Node.js und npm absichern. Die Befehle zu node, npm und nvm stehen in den Cheat-Sheets. Über die beiden anderen JavaScript-Laufzeiten habe ich in Anthropic und Bun und Deno geht zu Cloudflare geschrieben.

Quellen: Node.js-Sicherheitsmeldung vom 21. Oktober (öffnet in neuem Tab) · Brotli-Fix 0ed4268 (öffnet in neuem Tab) · Imperva: Node2Shell (öffnet in neuem Tab) · Aikido: Python-Pakete (öffnet in neuem Tab) · Patch the Planet: Brotli (öffnet in neuem Tab) · Node.js-Release-Plan (öffnet in neuem Tab) · NodeSource-Distributionen (öffnet in neuem Tab) (alle englisch) · heise: Node.js fixt eigene und geerbte Sicherheitslücken (öffnet in neuem Tab)