A runtime bug with a working exploit
Node.js shipped security releases on Wednesday 21 October: v22.23.4, v24.22.1 (Latest LTS) and v26.12.1 (Latest Release). They fix 10 CVEs, four rated High. The patch is two days old. What changed in the last 24 hours is that someone turned the worst of those CVEs into code execution on a stock build.
On 22 October, Ron Masas at Imperva published Node2Shell: attacker-supplied code running on the official Node.js 24.22.0 Linux arm64 image, in fresh containers, with no debugger and no instrumented binary. Node.js 24.22.1 rejected the same inputs. The name is a nod to this week's PHP2Shell, and the lesson is the same one: the vulnerable code sits in the engine, not in your app.
That is the part most teams will get wrong. You cannot fix this in package.json. The flaw is in the Brotli decoder that ships inside the Node binary, so npm audit, Dependabot and every lockfile scanner have nothing to flag. The fix is a new runtime on every host, image and CI runner you own.
What the bug is, in the advisory's words
The Node.js advisory describes CVE-2026-111497 plainly: "An attacker who can supply Brotli data that an application decompresses with node:zlib can corrupt process memory, which can lead to a crash, disclosure of process memory, or remote code execution." It affects the 26.x, 24.x and 22.x lines. The advisory credits "OpenAI Codex Security - Khai Tran and Sylvie Mayer" as reporters and mcollina and aduh95 as the fixers. Imperva's write-up credits the report differently; the advisory is the record, so go by that.
Two other lines in the advisory matter if you run anything old. "End-of-Life versions are always affected when a security release occurs." And Vercel's bulletin is blunter: Node.js 20 "will not receive this fix". If something in your estate still runs 20, it is now running a known memory-corruption bug in a decoder that may be fed by strangers. Moving it to a supported line is the fix.
The Hacker News reports that the advisory lists no workaround inside Node.js. There is no flag to flip and no environment variable that disables the decoder. Either the bytes never reach node:zlib, or you run a patched binary.
You are exposed in two directions
Imperva proved two attack paths. Both are ordinary code that nobody would flag in review.
Inbound: Express 5 decodes Brotli for you
The first path is a request body decoded by Express 5.0.0–5.3.0 or body-parser 2.0.0–2.3.0. Imperva got code to run through both raw() and json(). Imperva is careful to say that "a bare Node.js HTTP server does not decode incoming Brotli bodies simply by receiving them." Something has to decompress the body. In Express 5, that something is the default parsers.
This came in quietly. The Express 5 migration guide says express.json(), express.urlencoded(), express.text() and express.raw() "now support Brotli (Content-Encoding: br) decompression for incoming request bodies", and lists no migration steps for it. If you upgraded from Express 4, you probably never decided to accept Brotli request bodies from the internet. You accept them anyway. Any client that sets Content-Encoding: br gets its payload pushed through the vulnerable decoder before your route handler sees a byte.
Outbound: fetch() decodes every response
The second path runs the other way: a Brotli response read through Node's built-in fetch(). The attacker does not have to send you anything. They have to be on the other end of a request you make. Webhook delivery, link previews, URL importers, OAuth metadata discovery, scrapers, anything that fetches a user-supplied URL: if the server you call answers with a crafted Brotli body, your process decodes it.
This is the direction edge protection cannot help with. Vercel deployed WAF rules on 21 October that block the inbound path, and it says those rules do not cover "responses your functions fetch from other servers". A WAF sits in front of your app. A malicious response comes from behind it. Vercel's advice to self-hosted, container and CI users is to "upgrade to 22.23.4, 24.22.1 or 26.12.1 now, regardless of WAF protections."
How seriously to take it
Here is the honest picture, because the coverage is split on it.
The exploit is not reliable. Imperva says so itself: "The proof did not succeed on every attempt." It tested only arm64 builds and only its own app setups. According to The Hacker News, the earlier public proof of concept only reached code execution on a deliberately weakened build, and there was no exploitation seen in the wild as of 22 October.
None of that makes it safe to wait. An exploit that sometimes works on a stock official image is a working exploit. If a failed attempt only crashes a worker, your orchestrator restarts it and the attacker gets to try again. The advisory also lists disclosure of process memory as an outcome, and in a Node service that memory can hold tokens, session keys and database credentials. Reliability and other architectures tend to get solved once the method is public, and now it is.
The 10-minute audit, then the stopgap
This is the order I would work in. Most of it comes from Aikido's checklist.
1. Runtime version on every host, runner and image. Lockfiles will not tell you this. You need the actual binary in each place code runs. On hosts and CI runners, run node --version. For every image you deploy, not just the ones you built this week, run it inside the image:
node --version
docker run --rm "$IMAGE" node --version
Anything below 22.23.4, 24.22.1 or 26.12.1 on its line is vulnerable, and anything on 20 or older stays vulnerable for good. Imperva warns that "a package inventory can miss an older runtime bundled in a container", so check the images themselves rather than what your SBOM says.
2. Pinned base images. Aikido's line on this is the one to paste into your team channel: "A Dockerfile pinned to node:24.22.0 stays vulnerable no matter how often you rebuild it." Bump the tag, rebuild, redeploy. A floating tag only helps if the rebuild actually pulls the new image.
3. Express and body-parser majors. Find out which services take Brotli request bodies:
npm ls express body-parser
Express 5 or body-parser 2 in a public-facing service means the inbound path is open until the runtime is patched.
4. Direct Brotli calls. Search your code for brotliDecompress and createBrotliDecompress. Every hit is a place where you decode Brotli yourself, and you need to know where those bytes come from.
5. Outbound fetches to hosts you do not control. Make a list of every code path that calls fetch() on a URL a user or third party supplied. Those need a patched runtime first, because no proxy in front of you covers them.
6. Other runtimes and distro packages. Bun is fixed in 1.4.4. If you install Node from your Linux distribution, it may link the system Brotli library instead of the bundled one, in which case the fix arrives through the distro's Brotli package, not a Node update. Check both.
A stopgap for the inbound path
If some rebuilds will take days, reject Brotli-encoded request bodies at the reverse proxy until they land. Check your access logs for legitimate clients that send Content-Encoding: br first; if there are none, blocking it costs you nothing. A temporary rule in nginx:
if ($http_content_encoding ~* "br") {
return 415;
}
Be clear about what this buys you. It closes the inbound Express path and does nothing for outbound fetch(). Services that call user-supplied URLs need the patched binary, full stop.
The same Node.js release also refreshes the bundled CA certificates for the eMudhra compromise we covered earlier today. That is one more reason to rebuild on the new runtime today instead of waiting for the next routine image refresh.




Comments (0)
No comments yet. Start the conversation.