Node2Shell: Node.js Brotli RCE and the Fix
Node2Shell is the nickname for CVE-2026-111497, a High-severity out-of-bounds write in the Brotli decoder that ships inside Node.js. The fix is to update to Node.js 22.23.4, 24.22.1 or 26.12.1, depending on your line, and Node.js 20 gets no fix at all. Whether an attacker can actually reach the bug depends on one thing: does your app decompress Brotli data that someone else controls? I tested that on a vulnerable Node.js build, and the answer is narrower than the headlines suggest.
What Node2Shell is, and what is actually confirmed
Node.js published its October 2026 security release on October 21, fixing 10 CVEs. The Brotli one is the headline. According to the official advisory, the decoder treated an invalid block switch as "I need more input" instead of a permanent format error, so the streaming decoder kept appending bytes to an internal buffer that is only eight bytes long. An attacker who can feed Brotli data to node:zlib can corrupt process memory. The advisory lists three outcomes: a crash, disclosure of process memory, or remote code execution. It was reported by OpenAI Codex Security.
The "shell" in the name is a claim about the worst case, and the evidence for it is mixed. Here is where things stood as of October 25, 2026:
- A public proof of concept reached code execution only on a deliberately weakened test build. On a standard build its author reported a crash and a limited memory read.
- Sysdig's write-up says Imperva reproduced reliable RCE on the official Node.js 24.22.0 image on Linux arm64, while another team could not get blind remote execution to work with ASLR on. Some researchers argue the real impact is denial of service.
- The Hacker News reported no exploitation in the wild as of October 22.
My read: don't spend a week arguing about whether it is RCE or "just" a crash. A crash you can trigger remotely with one request is already a problem for a production API, and the patch is a minor version bump.
Fixed versions
| Release line | Status | Fixed in |
|---|---|---|
| Node.js 26.x | Current | 26.12.1 |
| Node.js 24.x | LTS | 24.22.1 |
| Node.js 22.x | LTS | 22.23.4 |
| Node.js 20.x | End of life | No fix, move to a supported line |
| Node.js 25.x | Not listed in the advisory | Treat as unpatched and move to 24 or 26 |
Bun 1.4.3 and earlier is affected too, and 1.4.4 is the fixed release. Check your version with this small script. It only reads process.versions, so it is safe to run anywhere:
// check-version.js
const FIXED = { 22: [22, 23, 4], 24: [24, 22, 1], 26: [26, 12, 1] };
const cur = process.versions.node.split('.').map(Number);
const fixed = FIXED[cur[0]];
if (!fixed) {
console.log(`Node ${process.versions.node}: line ${cur[0]} has no fix. Move to 22, 24 or 26.`);
} else {
const ok = cur[1] > fixed[1] || (cur[1] === fixed[1] && cur[2] >= fixed[2]);
console.log(`Node ${process.versions.node}: ${ok ? 'patched' : 'VULNERABLE, update to ' + fixed.join('.')}`);
}
I ran it in my sandbox, which happens to have an unpatched build:
Node 22.22.0: VULNERABLE, update to 22.23.4
Which of your code paths can an attacker reach?
This is the part most write-ups skip. The decoder is only dangerous where your app decompresses Brotli data an attacker controls. That can happen on two sides: request bodies coming in, and responses your server fetches from other servers.
I wanted to see this rather than trust a list, so I sent a valid, harmless Brotli-compressed JSON body ({"hello":"world"}, nothing malformed) to a local Express server with Content-Encoding: br, on the vulnerable Node.js 22.22.0 build. This only shows whether the body parser will decode Brotli at all. It does not exercise the bug.
// probe.js: does the body parser decode a Brotli request body?
const zlib = require('node:zlib');
const express = require('express');
const app = express();
app.use(express.json({ inflate: process.argv[2] !== 'noinflate' }));
app.post('/', (req, res) => res.json({ parsed: req.body }));
const srv = app.listen(0, async () => {
const body = zlib.brotliCompressSync(Buffer.from(JSON.stringify({ hello: 'world' })));
const r = await fetch(`http://127.0.0.1:${srv.address().port}/`, {
method: 'POST',
headers: { 'content-type': 'application/json', 'content-encoding': 'br' },
body,
});
console.log(r.status, (await r.text()).slice(0, 40));
srv.close();
});
| Setup | Result on my test | What it means |
|---|---|---|
Express 5.3.0, express.json() defaults |
200, body parsed | Brotli request bodies are decoded by default, so this path is reachable |
Express 5.3.0, express.json({ inflate: false }) |
415 | Compressed bodies are rejected before decoding |
Express 4.22.3, express.json() |
415, "unsupported content encoding br" | Express 4's body parser does not decode Brotli, so this path is not reachable |
Per Sysdig's research, other paths are reachable too: Koa body parsers, Fastify with @fastify/compress, any direct zlib.createBrotliDecompress() on untrusted data, and server-side fetch(), which decompresses Brotli responses automatically when the server answers with Content-Encoding: br. A bare http server does not decode incoming Brotli bodies on its own. I only tested the Express rows myself, so treat the rest as sourced, not verified.
So the honest summary is that the exposure is real but specific. Express 5 apps using the default JSON parser are in the open. Express 4 apps are not exposed through the body parser, but they can still be hit through any outbound fetch() to a server you don't control, like webhooks, URL previews or scrapers.
What to do today
- Check every place Node runs. Laptops, CI runners, servers, container images, and anything you ship as a single executable.
node --versionis enough. - Update and rebuild. Pulling a newer base image tag does nothing for a container that is already running. Rebuild from a patched base, redeploy, and restart. If your Dockerfile pins an old tag, it will quietly rebuild the vulnerable version, which is the kind of thing a multi-stage Dockerfile makes easy to review in one place.
- Ask your host if you don't control the runtime. Serverless and managed platforms patch on their own schedule. Vercel added firewall rules for inbound requests on October 21, but those don't cover Brotli responses your app fetches, and they don't replace the upgrade.
- If you truly can't patch yet, use these as a stopgap, not a fix. Set
express.json({ inflate: false })to refuse compressed bodies, but note it also refuses gzip, so test your clients first. RejectContent-Encoding: brat your proxy or load balancer. Audit outboundfetch()calls to servers you don't control. Run Node as an unprivileged user and keep secrets out of the process environment so a crash or compromise gets you less.
The Node.js advisory itself lists no workaround. Everything in step 4 comes from security vendors, so I would not lean on it for more than a few days.
The other nine CVEs matter more than the RCE debate
The same release fixed four High issues, and three of them have nothing to do with Brotli. CVE-2026-106069 and CVE-2026-105483 are TLS hostname-check bypasses involving odd IP address forms, and CVE-2026-105482 makes dns.resolve cut hostnames at an embedded null byte, so a different name gets resolved than the one you passed. If your service calls third-party APIs over TLS, those are arguably closer to your real risk than a memory bug you may not even expose. There are also three Medium and three Low issues, including a keep-alive response desync and a Permission Model bypass. I would patch for the TLS and DNS fixes alone. The full list is in the official advisory.
When this doesn't apply, and what else to check
If your app never decompresses Brotli from untrusted sources, Node2Shell itself is not an open door for you. You should still patch, for the reasons above, but you don't need an emergency at 2 a.m.
The bug is not only a Node.js problem, though. Aikido found that the Python Brotli and brotlicffi packages compile the same vulnerable decoder, and HTTP clients such as requests, httpx and aiohttp pick them up automatically when installed. Run pip show Brotli brotlicffi in each environment, and look for fixed releases Brotli 1.2.1 and brotlicffi 1.2.1.0. Upstream Brotli fixed it in 1.0.10, 1.1.1 and 1.2.1, so proxies and OS packages that bundle the library need their own updates. If this reminds you of the PHP flaws from earlier this month, it is a different bug with the same shape, and my PHP2Shell guide covers that one.
Frequently Asked Questions
Is Node.js 20 vulnerable to Node2Shell? The advisory lists only 22, 24 and 26, but it also says end-of-life versions are always affected when a security release lands. Node.js 20 is end of life with no patch, so treat it as vulnerable and upgrade.
Does sending Accept-Encoding: identity stop my server-side fetch from decoding Brotli?
No. Sysdig reports that fetch() decodes the response if the server sends Content-Encoding: br, even when you asked for identity. A malicious server controls that header, not you.
I updated my base image tag. Am I safe? Only after you rebuild and redeploy. A running container keeps the Node.js binary it started with.
Does this affect my frontend React app? Not directly, since this is the Node.js runtime. It matters for the Node servers behind your app, such as your API server.
Conclusion
Update to 22.23.4, 24.22.1 or 26.12.1, rebuild your containers, and move anything on Node.js 20 or 25 to a supported line. Do the check on every place Node runs, not just production, and patch the TLS and DNS fixes even if you decide your Brotli exposure is low. Next, tighten what your Express app accepts in the first place with this guide to a Content Security Policy with Helmet.
Last checked October 25, 2026. Sources: Node.js October 2026 security release, Sysdig, Aikido, The Hacker News. The Express results above come from my own test on Node.js 22.22.0, Express 5.3.0 and Express 4.22.3.
Related posts
Content Security Policy in Express With Helmet
Learn how to set up a Content Security Policy in Express with Helmet: nonces, report-only rollout, and the mistakes that weaken your XSS defense.
Debug a Node.js Memory Leak with AI
A practical AI prompt that uses a real heap snapshot retainer path to actually find a Node.js memory leak instead of just confirming one exists.
Fix OpenAI API Rate Limit Errors in Node.js
Get 429 rate_limit_exceeded errors from the OpenAI API in Node.js? Here is the real fix: exponential backoff, retry-after handling, and concurrency limits.
Node.js Worker Threads for CPU-Heavy Tasks
Learn how Node.js worker threads move CPU-intensive tasks off the event loop, with a working Express example and when to pick them over clustering.
0 Comments
No comments yet — be the first to share your thoughts.