A Brotli bug in Node.js 22, 24 and 26 is fixed in 22.23.4, 24.22.1 and 26.12.1. Check your server, patch it, and restart PM2 and Docker apps.
On 21 October 2026 the Node.js project released a fix for CVE-2026-111497, a Brotli decoder bug known as Node2Shell. If you run Node.js 22, 24 or 26 on a server, the job is a version bump to 22.23.4, 24.22.1 or 26.12.1, plus a restart of every app that was already running. This guide shows how to check your Node.js version on a server, how to update Node.js on Ubuntu with NodeSource packages, nvm or Docker, and why PM2 keeps serving the old version after an upgrade.
In short. CVE-2026-111497 (Node2Shell) is a bug in how Node.js decodes Brotli-compressed data. It is fixed in 22.23.4, 24.22.1 and 26.12.1. Run
node -v, compare the result with the table below, upgrade through the same channel you installed Node with (NodeSource packages, nvm, a Docker image), and then restart every app that was running before the upgrade. An upgrade never touches a process that has already started. Lines 18, 20 and 25 are end-of-life and get no patch. Facts in this article were checked on 27 October 2026.
Brotli is a compression format that browsers and servers use when an HTTP message carries Content-Encoding: br. Node.js ships a Brotli decoder. When that decoder meets an invalid block switch in the data, it treats it as "need more input" instead of a fatal error. Depending on the input, the result can be a crash, disclosure of process memory, or code execution. The bug was found by OpenAI Codex Security. The root sits in Google's Brotli library, versions 1.0.3 to 1.2.0 (fixed in 1.0.10, 1.1.1 and 1.2.1, according to NVD).
Two things are worth knowing before you plan the work. Severity ratings differ: High from the Node.js project and from Imperva, Critical (CVSS 4.0 score 9.2) from the author of the CVE, Google, on NVD. And exploitation in the wild was not confirmed as of 23 October 2026 (Sysdig): the bug is not in the CISA KEV list, and CISA's assessment on NVD says exploitation "none". We did not recheck that after 23 October. Calm, scheduled patching is the right response. Ignoring it is not.
The decoder only runs when something hands it Brotli data. The sources describe these paths:
node:zlib, Node's built-in compression module.fetch() responses on Node 24.22.0 (on 24.22.1 the data is rejected) and through request bodies in Express 5 with body-parser 2.x (raw() and json()).createBrotliDecompress().A bare Node http server does not decompress Brotli request bodies by itself, so it is not exposed by default. Your own code or a dependency may still do it. Mind the body-parser version: 2.x decompresses br by default (inflate: true), while 1.20.x handles only gzip and deflate.
Keep the evidence in proportion. Imperva's working code execution was shown on one setup (Node 24.22.0, Linux arm64, the official image) and did not succeed on every attempt. A crash is easy to reproduce. As for Next.js, Vercel says only this: "For self-hosted Next.js, upgrade Node.js; Next.js does not block Brotli responses in image optimization". We found no primary source claiming that ordinary Next.js request handling is vulnerable by default. The practical rule is the same for everyone: update Node.js under any app, especially one that accepts or requests Brotli data.
Line | Status | Version you need |
|---|---|---|
22.x | Patched on 21 October 2026 | 22.23.4 or newer |
24.x | Patched on 21 October 2026 | 24.22.1 or newer |
26.x | Patched on 21 October 2026 | 26.12.1 or newer |
25.x | End-of-life since 31 March 2026 | No patch: move to a patched line |
20.x | End-of-life since 24 March 2026 | No patch: move to a patched line |
18.x | End-of-life since 27 March 2025 | No patch: move to a patched line |
The Node.js project states it plainly: "End-of-Life versions are always affected when a security release occurs". A jump from 18 or 20 to a newer line is a major upgrade with its own breaking changes, so rehearse it on a spare server before you touch production. A spare server takes a few minutes to create: How to create a server in the HIP panel walks through it.
Start in an SSH session on the server. node -v prints the version your shell finds. which node shows the file behind it, and readlink -f follows symlinks to the real location (here both lines are the same file). apt-cache policy nodejs shows the installed package and the newest one available from your repositories.
Installed is what you run; Candidate is what apt would install. A higher candidate means an update is waiting (run apt-get update first so the list is fresh).nodesource1 tells you the package comes from the NodeSource repository.This tells you what a new process would start. It does not tell you what your running apps use. That gap is the whole point of the section about PM2 below.
The patch path depends on how Node got onto the machine, so identify that first.
Installed with | How to recognise it | How to patch |
|---|---|---|
NodeSource apt repository |
|
|
nvm (Node Version Manager) |
|
|
Distribution package | Version like | Depends on the distro, see below |
Docker image |
| Rebuild with a patched tag |
Distributions usually ship their own, often older, nodejs package, and the verdict here is not about the version number. Ubuntu's tracker lists nodejs on 22.04 and 24.04 as "Not affected": those packages use the system libbrotli, and the fix went into libbrotli1. On Ubuntu 20.04 the tracker says "Vulnerable". Debian's tracker marks nodejs in bookworm and trixie as vulnerable, even though its brotli packages are already fixed. These are tracker statuses on 27 October 2026 and they change. Candidates in clean images that day:
Release | nodejs candidate | Tracker status, 27 Oct 2026 | libbrotli1 candidate |
|---|---|---|---|
Ubuntu 22.04 | 12.22.9~dfsg-1ubuntu3.6 | Not affected | 1.0.9-2ubuntu0.1 |
Ubuntu 24.04 | 18.19.1+dfsg-6ubuntu5 | Not affected | 1.1.0-2ubuntu0.1 |
Debian 12 (bookworm) | 18.20.4+dfsg-1~deb12u3 | Vulnerable | 1.0.9-2+deb12u1 |
Debian 13 (trixie) | 20.19.2+dfsg-1+deb13u3 | Vulnerable | 1.1.0-2+deb13u1 |
On Ubuntu, apt-cache policy libbrotli1 shows whether the fixed library is installed. On Debian, treat the packaged Node as unpatched until the tracker says otherwise, and note that the packaged lines (18 and 20) are the end-of-life ones from the table above. The simplest way out is a supported line from NodeSource, nvm or Docker. For other releases, open the tracker for your release.
NodeSource is a third-party apt repository with current Node.js builds. Its node_22.x repository carries 22.23.4-1nodesource1; on our stand apt-cache policy nodejs reported it as the candidate. Run the two commands as root or with sudo. The first refreshes the package lists, the second upgrades nodejs only if it is already installed, so it cannot install anything by accident:
apt-get updateapt-get install --only-upgrade -y nodejsThe output of the second command is long. On some servers it ends with an unrelated note, such as one about rebooting for a new kernel. That note is not about Node. What matters is the check afterwards:
The first command shows the update waiting (run it before the upgrade). The second prints the new version once the upgrade is done. A new shell now gets the patched Node. Your running apps do not, and we get to that shortly.
nvm installs Node.js versions into your home directory and switches between them per shell. It does not use apt. nvm install 22 fetches the latest 22.x. In our run it downloaded the tarball from nodejs.org and confirmed the checksum:
New shells will not use it automatically until you set the default alias. Also look at what nvm ls says: the old version stays installed next to the new one.
The path contains the version number. If a service file or a PM2 command points at the old directory (.../v22.23.2/bin/node), that app keeps the old Node until you change the path. Check where yours points.
A Docker image is a packaged template with Node.js already inside; a container is a running copy of it. Upgrading Node on the host does nothing for containers: each carries its own. So the fix is a new image and a fresh container. First look at what is running. The loop below asks every running container for its Node version; || echo no-node covers containers that have none.
The Dockerfile pins an exact tag, node:22.23.2-alpine, so a rebuild alone changes nothing: the tag still means 22.23.2. You have two options. An exact patched tag (node:22.23.4-alpine; tags 22.23.4, 24.22.1 and 26.12.1 exist with the -slim, -alpine, -bookworm and -trixie variants) gives repeatable builds, but you must edit it again for the next release. A floating tag such as node:22-alpine already points at the fixed build (we confirmed v22.23.4) and picks up future fixes on rebuild. Our opinion: use the floating tag for small services you rebuild often, and the exact tag where you need reproducible builds. Our demo switches to the floating one and rebuilds with --pull, which tells Docker to check for a newer base image instead of reusing a cached one:
A built image changes nothing until a container runs from it. docker restart would reuse the old image, so remove the container and start a new one. docker rm -f discards anything stored only inside the container; keep your data in volumes. The port mapping and name below belong to the demo, so use your own.
PM2 is a process manager for Node apps: it starts them, restarts them after a crash and keeps a list. A running Linux process keeps the executable it started from. When apt replaces /usr/bin/node, the old file is gone from the disk, but the app that is already running keeps working from it. The upgrade looks done (node -v says 22.23.4) while the app stays on the vulnerable build.
We reproduced it with a tiny app that answers with its own process.version, started by PM2 on 22.23.2. The first command in the card ran before the upgrade, the second and third after it. /proc/PID/exe is the link to the file a process was launched from, and (deleted) means that file no longer exists under that name. You get the PID from pm2 pid <name>; n2s is the name of our demo app, so use your own.
This is the trap: a green node -v in your shell and ok v22.23.2 from the app, on the same machine, at the same minute. The fix is to restart the app. After pm2 restart the app answers with the new version and the new process points at a normal file:
PM2's own background daemon is also a long-running process, so after an upgrade it is worth refreshing it too with pm2 update. In our run the app came back online with uptime 0s and a new PID; the restart above was already enough to get the new Node into the app.
The same logic holds for anything long-running that started before the upgrade: a systemd service, a worker, a script in a screen session. Restart it the way you normally do. A habit that pays off: make your app report process.version somewhere (a health endpoint, a startup log line), exactly as our demo does. Then you can check the running process, not the file on disk. If you need to find out which process holds which port, ss instead of netstat covers it.
Only the update closes the hole. Node.js itself publishes no list of workarounds. Sysdig and Imperva name partial measures that reduce exposure while you schedule the work. None of them replaces the update:
express.json({ inflate: false }). Compressed bodies then get HTTP 415 (unsupported media type). Check your clients first: anything that sends compressed bodies will start failing.Content-Encoding: br on incoming requests at your proxy.These do not cover every path from the list above. A fetch() to a server that returns Brotli, for example, is untouched by an incoming-request rule. If you have not yet done the basics on this server, the first-hour hardening guide is the place to start.
The ratings differ. The Node.js project and Imperva rate it High; the author of the CVE, Google, rates it Critical with a CVSS 4.0 score of 9.2 on NVD. Which paths are reachable depends on your app, so patch on schedule and do not wait for the rating to settle.
As of 23 October 2026, Sysdig reported no confirmed cases, the bug is not in CISA's KEV list, and CISA's assessment on NVD says exploitation "none". We did not recheck after that date, so look at the sources again before you decide how urgent it is.
A bare Node http server does not decompress Brotli request bodies by itself. For self-hosted Next.js, Vercel's advice is to upgrade Node.js, and it notes that Next.js does not block Brotli responses in image optimization. No source we read says ordinary Next.js request handling is vulnerable by default. Update Node anyway: your dependencies decide what is reachable.
Because the running process keeps the old binary until it restarts. Run pm2 restart for each app (or restart your service), and rebuild and recreate Docker containers. Then check the version from inside the running app.
Those lines are end-of-life and get no patch from the Node.js project. Move to a patched line (22.23.4, 24.22.1 or 26.12.1) after a test run on a spare server. If a distribution package is what you run, check your release in the tracker, as the table above shows.
node -v: check /proc/PID/exe for (deleted) or have the app report process.version.