Node2Shell: Node.js Brotli RCE and the Fix

Published · 19 views
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

  1. Check every place Node runs. Laptops, CI runners, servers, container images, and anything you ship as a single executable. node --version is enough.
  2. 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.
  3. 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.
  4. 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. Reject Content-Encoding: br at your proxy or load balancer. Audit outbound fetch() 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.

#nodejs #express #node2shell #brotli #cve-2026-111497 #security
Aliyan Faisal

Written by

Aliyan Faisal

Software engineer and AI developer. I build LLM-powered products, RAG pipelines and automations, and the web apps behind them.

Get new posts in your inbox

The best new articles, daily, twice a week or weekly. Choose your topics. No spam, unsubscribe anytime.

Prefer an app? Get posts as they go live.

0 Comments

No comments yet — be the first to share your thoughts.

Leave a comment

Never published.