IONSEC
Resources
Articleresearch · threat intelligence

Node2Shell: One Brotli Bug, Reached From Both Sides of the Wire

CVE-2026-111497 is an out-of-bounds write in the Google Brotli decoder that Node.js bundles. Attackers reach it inbound, through request bodies that Express 5, Koa and @fastify/compress decompress by default, and outbound, through any response fetch(), axios or got decodes for you. Below: the exposure map, the version traps, what the proofs of concept actually show, and the checklist.

IONSEC Team

Published

Read

17 minutes

One Brotli bug, two directions

One bug in one shared Brotli decoder, reached from both directions of an HTTP exchange: an unauthenticated request whose body is sent with Content-Encoding: br, or the response when the application fetches a URL an attacker controls. An edge WAF only ever sees the first. Both end in the same two steps — an invalid block switch the unfixed decoder reports as “needs more input” instead of a format error, then bytes appended past an 8-byte buffer — and the same terminal: memory corruption inside the Node process.
  • Entry — nothing held yet
  • Edge — where a WAF sees the request
  • Attacker-controlled stage
  • Path completes
  • Corruption past an 8-byte buffer
  • Memory corruption in the Node process

On 21 October 2026 the Node.js project shipped 22.23.4, 24.22.1 and 26.12.1, fixing ten vulnerabilities in one release. Nine are Node’s own. The one that leads the release is not. CVE-2026-111497 is rated High by Node.js and 9.2 under CVSS 4.0 by Google, which issued the CVE. The bug lives in the Brotli decompressor Google maintains and Node vendors under deps/brotli. Node’s advisory sums it up: an invalid block switch was reported as a temporary need for more input instead of a permanent format error, so the streaming decoder kept appending bytes to an eight-byte internal buffer. Anyone who can hand an application Brotli data that it decompresses with node:zlib can corrupt process memory. The result can be a crash, a memory disclosure, or code execution.

The name arrived a day later. Node2Shell is what Imperva’s red team called its analysis when it ran code on the official Node.js 24.22.0 Linux arm64 image. The pattern echoes PHP2Shell, the PHP core bugs disclosed the same day Node shipped its fix. Aikido then called the wider blast radius “HTTP2Shell”, and with reason: this decoder is not Node’s. Bun, Python’s Brotli bindings, Envoy and a long tail of ports and wrappers carry the same code.

The one-sentence version. Node2Shell is reachable from both directions of an HTTP exchange. It comes inbound through request bodies that Express 5, Koa and @fastify/compress decompress by default, and outbound through any response that fetch(), axios or got decodes for you. Meanwhile the upstream fix sat unlabelled in Brotli’s public repository for eight months before anyone shipped it as a security release.

Patching is the fix. What follows is the map: who is exposed and why version strings mislead, how the bug works, how far it reaches beyond Node, what the wild has shown, and what to do while you get there.

Am I exposed?

Two things have to line up: a vulnerable decoder, and a path for attacker bytes to reach it. The first is decided by the runtime, never by an npm package. The npm package named brotli is not the vulnerable component, and npm audit will not flag a runtime bug.

Versions. Every supported line had the flaw until 21 October:

Runtime / lineVulnerable throughFixed releaseNotes
Node.js 2626.12.026.12.1
Node.js 2424.22.024.22.1Imperva’s code-execution target
Node.js 2222.23.322.23.4
Node.js 25, 23, 20 and olderend of lifeno fixEvery EOL line we checked, back to Node 12, carries the vulnerable early return. Node 20 left support on 30 April 2026
Bun1.4.31.4.4fetch() asks for and decodes Brotli by default, with no verified switch
Brotli library1.0.3–1.0.9, 1.1.0, 1.2.01.0.10, 1.1.1, 1.2.1The vulnerable check dates from 2018

The version traps. Two of them, and both make inventories lie.

  • process.versions.brotli tells you nothing. Node computes it from Brotli’s version constant, which the fix does not touch. Patched 24.22.1 and 26.12.1 report 1.2.0, and patched 22.23.4 reports 1.1.0, which is exactly what the vulnerable builds before them report.
  • A distribution’s Node may not use its bundled decoder. Debian’s nodejs packages link the system libbrotli. Its fix arrives as brotli 1.0.9-2+deb12u1 in bookworm (DLA-4855-1) and 1.1.0-2+deb13u1 in trixie (DSA-6582-1). The Debian tracker nevertheless lists every nodejs row as “vulnerable”, and forky and sid were still unfixed at 1.2.0-4 when we checked. Alpine’s nodejs-current bundles its own copy and ships the fix as 26.12.1-r0, with Alpine’s brotli packages fixed at 1.2.1-r0 (1.1.1-r0 on 3.21). On 23 October Ubuntu listed every brotli and nodejs row as “Needs evaluation”. Amazon Linux shows brotli as “Pending Fix” and nodejs/nodejs20 as “No Fix Planned”.

Managed platforms lag the upstream release, too. A Microsoft Q&A thread reported Azure Functions Flex Consumption in East US 2 still running 22.23.2 after 22.23.4 shipped. A Vercel community thread reported a managed 24.x project still on 24.21.0 after a redeploy. AWS Elastic Beanstalk and App Runner runtimes were listed on pre-fix versions at disclosure. Read process.version from inside the running function, not from the platform’s marketing page.

Inbound: who decodes Content-Encoding: br request bodies. Any client can send one, and no authentication is needed. These rows come from reading each package’s source at its current release tag:

StackDecodes Brotli request bodies?
Express 5 / body-parser 2.x (json, urlencoded, text, raw)Yes, by default. inflate defaults to true
Koa parsers on co-body ≥5 / inflation 2.1.0 (@koa/bodyparser ≥5, koa-bodyparser 2.4–2.5 and 4.1+, koa-body ≥2.4)Yes, by default. The only switch is not parsing those types
Fastify with @fastify/compress ≥5Yes, on every route once the plugin is registered, even if you added it only to compress responses
NestJS 11/12 on Express, Apollo Server 5.5 standalone, Parse Server 9.10, Strapi 5.59, n8n 2.44’s /api/v1Yes. Apollo’s standalone server has no switch, and its 50 MB limit applies to decoded bytes
Express 4 / body-parser 1.x (and apps on it, such as Directus, Node-RED and Ghost)No, answers 415
hapi, restify, Fastify core, h3/Nuxt, SvelteKit, Hono on Node, Next.js Pages API routes, gRPC-jsNo
A bare node:http serverNo. It never decodes bodies on its own

Outbound: who decodes Brotli responses. This is the direction most teams will miss. No inbound request carries the payload, so no WAF in front of the application can see it.

  • Node’s built-in fetch() (undici) adds br to Accept-Encoding on every HTTPS request. It decodes any response that declares Content-Encoding: br, on plain HTTP too, because the server decides the encoding, not the client.
  • axios from 1.0, got, node-fetch 3 and needle decode Brotli by default. superagent decodes it even though it never asks for it.
  • npm’s own fetch stack (minipass-fetch) decodes Brotli responses. A malicious or intercepted registry or tarball host therefore reaches the decoder on the machine running npm install.
  • The highest-likelihood outbound paths are features that fetch URLs an attacker chooses: webhooks, link unfurlers, oEmbed, image and avatar proxies, crawlers, and server-side rendering that fetches user-supplied URLs. Others include Next.js remote image optimisation when remotePatterns is set (the default list is empty) and http-proxy-middleware’s responseInterceptor.

How the bug works

The flaw is in DecodeBlockTypeAndLength() in Brotli’s c/dec/decode.c. A Brotli stream declares how many block types it uses. If it declares one and then asks to switch blocks, the stream is invalid. Since a 2018 change (the “Large Window Brotli” commit, first released in 1.0.3), the decoder answered that case with a plain false:

   if (max_block_type <= 1) {
-    return BROTLI_FALSE;
+    return BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH;
   }

The decoder has a fast path and a careful “safe” path for when input may run short. In the fast path that false was simply ignored. In the safe path, the BROTLI_SAFE wrapper turns any false into NEEDS_MORE_INPUT, meaning “this stream is fine, it just ran out”. On that exit, BrotliDecoderDecompressStream() saves the unread tail of the input into s->buffer, an eight-byte union in the decoder state. It exists to hold the few leftover bytes when input really runs out, and in release builds the copy has no bounds check. A spurious “needs more input” with plenty of input left turns that copy into a write past the buffer, into the fields that follow it in the state structure. Those fields are the buffer length, positions, ring-buffer size and mask, the distance ring, the error code, and then the ring-buffer and Huffman-table pointers. Code execution needs that write to reach the pointers in a way the attacker controls. That is the exploit’s work, and the reason reliability varies.

The fix turns the case into a permanent format error, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (-17), and makes the real need-more-input cases say so explicitly. Node’s fixed releases also expose that constant as zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH. Node’s regression test feeds a 22-byte stream through brotliDecompressSync() and through a stream written one byte at a time, and expects ERR__ERROR_FORMAT_BLOCK_SWITCH from both. Two consequences matter for defenders. That 22-byte stream is now the most copied artefact in the public proof-of-concept repositories. And only a fixed decoder can emit that error code, a fact we return to in detection.

The timeline is the uncomfortable part. Google’s fix, “Detect and report invalid stream earlier”, landed on Brotli’s master branch on 27 February 2026, with no security label. Node’s pre-alert went out on 14 October. Node shipped its cherry-pick and Brotli shipped 1.0.10, 1.1.1 and 1.2.1 on 21 October. As Hadrian points out, anyone diffing Brotli’s history had the code change eight months before the advisory explained why it mattered. Node credits OpenAI Codex Security (Khai Tran and Sylvie Mayer) with the report and Matteo Collina and Antoine du Hamel with the fix.

Beyond Node: the HTTP2Shell widening

The CVE is assigned to Google Brotli, not Node.js, and everything that vendors or links its C decoder inherits it. Several ecosystems have no fixed release at all:

ComponentExposureFixed in
Bunfetch() sends Accept-Encoding: gzip, deflate, br, zstd and decodes; also node:zlib and DecompressionStream1.4.4 (pins Brotli 1.2.1)
Python Brotli (PyPI)Bundles the C decoder. Every PyPI release from 1.0.4 to 1.2.0 is affected, with no backport line (1.0.10 and 1.1.1 exist only as GitHub tags)1.2.1 only
Python brotlicffi1.0.9.0 through 1.2.0.2 bundle affected decoders1.2.1.0
aiohttp serversDecode Content-Encoding: br request bodies by default when Brotli is installed. It is the only Python server stack in the survey that doesUpgrade Brotli
requests, httpx, urllib3, pip, ScrapyDecode Brotli responses automatically whenever a Brotli module is importable; Scrapy always doesUpgrade Brotli. The brotli>=1.2.0 pins in urllib3’s extra and in Scrapy still admit 1.2.0
Envoy (Brotli decompressor filter)GHSA-3wvj-j6qm-62mr1.36.12, 1.37.8, 1.38.6, 1.39.3, 1.40.0. Their release notes never mention Brotli
Go andybalholm/brotli (pure-Go translation)Same logic through v1.2.6v1.2.7
RubyGems brotli / Maven brotli4j0.4.0–0.9.0 / 1.21.0–1.23.0 (older releases unchecked)0.9.1 / 1.24.0
PECL brotli (PHP), .NET runtime (vendors Brotli 1.1.0)Explicit decompression callsNo fixed release found
Rust brotli-decompressorThe same early return, still present in 6.1.1None. Safe Rust should turn it into a panic rather than corruption (our inference, untested)
Cloudflare workerd (public source)Brotli content-encoding on by default for Workers with a compatibility date from 2024-04-29, vendoring 1.2.0Not in the public build as of 24 October. Hosted status unpublished
ElectronThe Node side of 45.0.0, 44.9.3, 43.8.3 and 42.11.18 predates the fixOnly 46.0.0-alpha.1 carries Node 24.22.1
DenoRust decoders, not the C libraryUnresolved, not cleared
nginx ngx_brotli, Apache mod_brotliEncoder onlyNot affected

The Python row deserves its own sentence. GHSA-9v8h-qqgm-4m35 and PYSEC-2026-4322/4323 now cover both PyPI packages. But a constraint like brotli>=1.2.0, the one in urllib3’s extra and in Scrapy, still accepts the vulnerable 1.2.0, and any lockfile written before 22 October pins it. Meanwhile a crawler built on Scrapy decodes Brotli from every site it visits.

What the wild has and has not shown

The proofs of concept. Imperva is the anchor. In fresh containers running the official Node.js 24.22.0 Linux arm64 image, its standalone proof of concept ran code carried in a Brotli response consumed by fetch(), and in request bodies parsed by Express 5.0.0–5.3.0 and body-parser 2.0.0–2.3.0. Its own caveat: “execution took multiple attempts in our setup”. 24.22.1 rejected the same input, and no 22.x or 26.x build was tested. The first public proof of concept, also on 22 October, reached code execution only on a deliberately weakened test build. According to The Hacker News, on stock builds it produced crashes and small memory reads.

By 24 October GitHub held more than forty repositories. The earliest we found, CaterMaoZi’s, appeared on 21 October, a day before the CVE record was even published. dimasma0305’s claims black-box code execution against Node 24.22.0 with Express 5.3.0 / body-parser 2.4.0 on x86_64, under stated limits: an exact binary, a single worker, a restart supervisor. lesterlam’s independent reproduction documents that the naive overwrite of the buffer index does not survive. Many of these repositories share one artefact, Node’s own 22-byte regression stream, and so do the defensive self-tests that check whether a runtime is patched. Seeing that stream on the wire tells you someone is testing. It does not tell you who.

Exploitation: none confirmed. CISA’s assessment on the CVE record, added 23 October, reads exploitation none, automatable no, technical impact total. The KEV catalog release of 23 October does not list it. NCSC-NL rates the likelihood of abuse medium and the damage high, with no known active abuse. GovCERT.HK’s “High Threat” alert notes that proof-of-concept code has been reported. We found no GreyNoise tag, honeypot report, Metasploit module or internet-exposure count. Unlike PHP2Shell, there is no public spray of attempts to count yet. The precondition, a code path that decodes Brotli, is invisible to mass scanning. The outbound path needs no scanning at all: a webhook or link-preview feature is the invitation.

WAF coverage, mostly inbound. Vercel deployed rules for every project on 21 October and says plainly, “Only the runtime patch covers fetched responses.” Imperva added rules validated against the attempts it reproduced. Indusface’s AppTrana blocks Brotli-encoded request bodies and duplicate Content-Encoding headers. Cloudflare’s WAF changelog through 22 October lists no Node2Shell rule; its emergency rules that week were the PHP2Shell set. A repository search on 24 October found no rule for this CVE in Emerging Threats, SigmaHQ, the OWASP CRS, Nuclei templates, Elastic’s detection rules or Corelight’s community detections. Tenable’s plugin 370490 checks versions.

What to do now

Patching is the fix. Everything else below narrows the paths while you get there, one direction at a time.

1. Patch, restart, rebuild. Targets are Node.js 22.23.4, 24.22.1 or 26.12.1, and Bun 1.4.4. A patched binary nobody has loaded protects nobody, so restart every process. The official node Docker images were rebuilt for the fixed releases on 21–22 October, but images pinned by digest or old tag stay vulnerable until rebuilt. EOL lines (25, 23, 20 and older) have no fix: move them.

2. Verify the decoder, not the label. Check the runtime that serves traffic, not the one on your PATH:

# version of the Node that actually runs the service
node -p process.version

# -17 on official 22.23.4 / 24.22.1 / 26.12.1, undefined on builds without Node's fix
node -p "require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH"

# does this Node link the system Brotli? If true, the distro package decides
node -p "process.config.variables.node_shared_brotli"

# Debian/Ubuntu: the brotli changelog carries the CVE id once fixed
zgrep CVE-2026-111497 /usr/share/doc/libbrotli1/changelog.Debian.gz

# RHEL/Fedora/Alma/Rocky
rpm -q --changelog brotli | grep CVE-2026-111497

The constant check covers Node’s bundled decoder. A distribution build that links the system library can be fixed through its brotli package while that constant stays undefined, so follow the third command.

3. If you cannot patch today, cut the inbound path. Reject request bodies whose Content-Encoding contains br before any parser sees them, at the proxy or in the application:

# nginx: refuse Brotli-encoded request bodies
if ($http_content_encoding ~* "(^|,)\s*br\s*(,|$)") { return 415; }
// Express/Koa-style guard, mounted before any body parser
app.use((req, res, next) => {
  const ce = String(req.headers['content-encoding'] || '').toLowerCase();
  if (ce.split(',').some((t) => t.trim() === 'br')) return res.status(415).end();
  next();
});

Other options at the framework level:

  • express.json({ inflate: false }) works too, but it rejects gzip and deflate as well.
  • On Fastify, set @fastify/compress to globalDecompression: false, or list requestEncodings without br.

Before you block, inventory legitimate senders. They are rare but real: Apify’s JavaScript API client, for one, brotli-compresses request bodies of 1 KB and over by default, according to its documentation. Matching must be case-insensitive and catch br inside lists such as gzip, br.

4. Cut the outbound path for code that fetches untrusted URLs. Here the runtime patch is the only complete fix. Changing Accept-Encoding alone is not enough, because a malicious server can send Brotli whether you asked for it or not. In the interim, route webhook senders, unfurlers, image proxies and crawlers through an egress proxy that refuses responses carrying Content-Encoding: br.

5. Python, Bun and the rest.

  • Python: upgrade to Brotli 1.2.1 and brotlicffi 1.2.1.0, and check lockfiles rather than constraints. aiohttp servers with Brotli installed are an inbound path, and a middleware is too late to stop it: the parser decompresses before any middleware runs. Until you can upgrade, pass handler_args={"auto_decompress": False} to web.Application (it turns off gzip request decompression too). Scrapy, requests and httpx are outbound.
  • Bun: upgrade to 1.4.4. We found no verified switch to turn decoding off.
  • Envoy: upgrade if you configure a Brotli decompressor.
  • Ports: Go andybalholm/brotli v1.2.7, RubyGems brotli 0.9.1, brotli4j 1.24.0.
  • No fix yet: PECL brotli, .NET and Rust brotli-decompressor have none. Do not decompress untrusted Brotli with them.
  • Electron: until a stable line carries Node 24.22.1, do not decode untrusted Brotli with Node APIs in the main process.

6. If you were exposed before you patched, work it as an incident. Look for Node or Bun crash loops shortly after Brotli-encoded requests or outbound fetches, server processes spawning shells, and new files or scheduled tasks owned by the service account. The advisory names process-memory disclosure as an outcome, so rotate secrets the process held.

Detection

Exploitation happens inside the decoder, below application code. The application usually sees an error, or nothing at all. The signal lives in four places: the inbound request, the application’s error log, the host, and, for the outbound path, the egress proxy.

  • Requests with Content-Encoding: br. These are rare enough to review. They are especially worth a look when combined with Transfer-Encoding: chunked, or with duplicate Content-Encoding headers. Never alert on outbound Accept-Encoding: br: undici, axios, got and node-fetch send it as a matter of course.
  • The public streams. Node’s 22-byte regression vector 200010414141fcffff07002228f00b60f7f41f000000, and the three variants copied across the PoC repositories, appear in raw request bodies or, for the fetch path, in Brotli responses.
  • Scanner shapes. Ostorlab’s module POSTs a plain {"ping":1} with Content-Encoding: br, a 14-byte valid stream and the regression vector to /, /api, /api/v1 and /graphql. Self-identifying checkers use user agents such as Node2ShellChecker/…, cve-2026-111497-verify and Mozilla/5.0 (compatible; security-scanner).
  • The error a patched decoder throws. ERR__ERROR_FORMAT_BLOCK_SWITCH (errno -17) can only come from a fixed decoder, so seeing it in your logs means an attempt reached a host that was already patched. Unpatched decoders answer the public vector with ERR__ERROR_FORMAT_BLOCK_LENGTH_1 (-9), which is weak on its own: ordinary corrupt input produces it too. Decoders built from Brotli 1.0.x, Debian bookworm’s system library among them, name their errors without the prefix, so on those hosts expect ERR_BLOCK_SWITCH and ERR_BLOCK_LENGTH_1.
  • The host. Look for Node or Bun crashes right after Brotli traffic (Imperva: a triage clue, not a stand-alone sign), server processes spawning sh -c id or whoami, and responses carrying uid=… gid=…, the marker one public PoC prints.

The cheap greps:

ERR__ERROR_FORMAT_BLOCK_SWITCH                — attempt against a patched decoder
ERR_BLOCK_SWITCH                              — the same, on decoders built from Brotli 1.0.x
ERR__ERROR_FORMAT_BLOCK_LENGTH_1              — what unpatched decoders return on the public vector
segfault at … in node / exit code 139         — crash after br traffic or an outbound fetch
Content-Encoding: br on POST/PUT/PATCH        — only if your proxy logs request headers

Released with this post: node2shell-detector. It packages the checks behind this section as one standard-library-only Python CLI:

  • Runtime posture: every node, bun and deno on the host, plus Python environments where Brotli or brotlicffi sit beside aiohttp. Verdicts ignore process.versions.brotli and follow shared-Brotli builds down to the distribution package.
  • Decoder fingerprint: reads Node and Bun binaries, libbrotlidec, Python extensions and native addons for the error name the fix introduced, in both the 1.1+ and the 1.0.x spelling. A missing string table is reported as unknown, never as patched.
  • Dependency scan: reads npm, yarn and pnpm lockfiles, plus Python, Go, Rust, Ruby, Java and Dockerfile pins. It names which packages decode Brotli request bodies or responses, and escalates when the same host runs a vulnerable runtime.
  • Log scan: checks access, application and system logs against the catalogue above.
  • Opt-in self-test: runs Node’s own regression vector in a throwaway child process.

The network side ships in the same repository:

  • 14 Sigma rules;
  • 14 Suricata rules in a private block from SID 1006600;
  • a ModSecurity/Coraza drop-in that detects by default and blocks on request;
  • seven YARA rules;
  • inbound snippets for nginx, Apache, HAProxy, Envoy, Cloudflare, AWS WAF and Azure;
  • egress guards for HAProxy, Envoy and nginx;
  • request guards for Express, Koa, Fastify, NestJS and aiohttp.

Every fingerprint is copied from a fetched source or derived from the fix diff, and is labelled as one or the other. The references file keeps the provenance where you can check it.

What this actually says

Silent fixes in shared libraries are a disclosure in waiting. The patch was public for eight months as an unremarkable “invalid stream” fix. Downstream, Envoy’s fixed releases picked it up by pinning a newer Brotli snapshot, and their release notes never say so. Teams that track advisories rather than diffs, which is nearly everyone, learned about it on 21 October. Attackers who diff libraries had the change since February.

Your attack surface includes what your dependencies decode for you. Nobody writes zlib.brotliDecompress(req.body). Express 5 does it because inflate defaults to true. @fastify/compress does it on every route because request decompression ships enabled. Node’s fetch() does it because the server said br. “Decodes by default” belongs on the same list as “parses by default” in code review. Outbound fetches of user-chosen URLs belong behind an egress proxy, for this bug and the next one.

Inventories keyed on version strings will keep failing. The version a patched Brotli reports is unchanged. A Debian Node’s safety lives in a different package, and Electron’s decoder lives in two places at once. Check the decoder itself, or the release that ships it.

AI-assisted discovery is now a supply line. This is the second core-runtime bug this week with an OpenAI Codex Security credit: the PHP Foundation’s account of PHP2Shell names Codex Security as reaching that root cause independently. Expect more eight-year-old bugs in libraries everyone vendors, and plan the patch pipeline for it.

What to keep, concretely:

  • Restart after patching and rebuild images.
  • Treat any parser or client that decodes on your behalf as attack surface.
  • Put features that fetch user-supplied URLs behind egress controls.
  • Keep ERR__ERROR_FORMAT_BLOCK_SWITCH on a dashboard: it is the cheapest proof that someone tried.

References

  1. Node.js — Wednesday, October 21, 2026 Security Releases, the advisory text and the fixed lines — https://nodejs.org/en/blog/vulnerability/october-2026-security-releases
  2. CVE-2026-111497 — record by Google (CNA): affected Brotli 1.0.3–1.2.0, CVSS 4.0 9.2, CWE-787/CWE-252 — https://www.cve.org/CVERecord?id=CVE-2026-111497
  3. google/brotli — Detect and report invalid stream earlier, commit 0ed4268a, 27 February 2026 — https://github.com/google/brotli/commit/0ed4268a4401abcb4472096340477cf4af7d1a2b; security releases 1.0.10, 1.1.1, 1.2.1 — https://github.com/google/brotli/releases
  4. GitHub Advisory Database — GHSA-9v8h-qqgm-4m35, the PyPI Brotli and brotlicffi ranges — https://github.com/advisories/GHSA-9v8h-qqgm-4m35
  5. Imperva — Node2Shell: From Brotli Data to Remote Code Execution, 22 October 2026 — https://www.imperva.com/blog/node2shell-from-brotli-data-to-remote-code-execution/
  6. Aikido — Node2Shell: Node.js Brotli vulnerability allows remote code execution and the HTTP2Shell follow-up — https://www.aikido.dev/blog/node2shell-nodejs-brotli-rce-cve-2026-111497, https://www.aikido.dev/blog/python-packages-vulnerable-node2shell-brotli-cve-2026-111497
  7. OpenSourceMalware — Node2Shell affects Python, the PyPI testing and fixed releases — https://opensourcemalware.com/blog/node2shell-affects-python
  8. Bun — issue #46352 and the 1.4.4 release — https://github.com/oven-sh/bun/issues/46352, https://bun.com/blog/bun-v1.4.4
  9. Vercel — Node.js security advisory: Brotli decoder RCE (CVE-2026-111497) — https://vercel.com/kb/bulletin/node-js-security-advisory
  10. Hadrian — Node2Shell: What we know about the Node.js Brotli RCE, the eight-month timeline — https://hadrian.io/vulnerability-alerts/node2shell-what-we-know-about-the-node-js-brotli-rce-cve-2026-111497
  11. Debian — security tracker and DLA-4855-1 — https://security-tracker.debian.org/tracker/CVE-2026-111497, https://lists.debian.org/debian-lts-announce/2026/10/msg00049.html
  12. NCSC-NL — NCSC-2026-0453: Kwetsbaarheid in Google Brotli-decoder, 23 October 2026 — https://advisories.ncsc.nl/2026/ncsc-2026-0453.html
  13. oss-security — Re: Upcoming Node.js security release 2026/10/21, the Envoy downstream note — https://www.openwall.com/lists/oss-security/2026/10/23/25
  14. Indusface — Node2Shell (CVE-2026-111497): Node.js Brotli RCE, the AppTrana rule update — https://www.indusface.com/blog/cve-2026-111497-node2shell-nodejs-brotli-rce/
  15. Express — Migrating to Express 5, Brotli request-body support — https://expressjs.com/en/guide/migrating-5.html

Frequently asked questions

Is my Node.js app exposed to Node2Shell if I never use Brotli myself?

Possibly. Your dependencies may decode Brotli for you. Express 5 with body-parser 2, Koa parsers built on co-body 5 or later, @fastify/compress, NestJS 11 and Apollo Server 5 standalone all decompress Content-Encoding: br request bodies by default. fetch(), axios 1.x, got and node-fetch 3 decode Brotli responses automatically. A bare node:http server never decodes bodies on its own, but upgrading Node.js is the fix either way.

Which versions fix CVE-2026-111497?

Node.js 22.23.4, 24.22.1 and 26.12.1, released on 21 October 2026, fix it. Bun fixed it in 1.4.4. The Brotli library fixed it in 1.0.10, 1.1.1 and 1.2.1. On PyPI the fixes are Brotli 1.2.1 and brotlicffi 1.2.1.0. Node.js 25, 23, 20 and older lines are end of life and get no fix.

Why does process.versions.brotli show the same value after patching?

The fix does not change Brotli's version constant. Patched Node.js 24.22.1 and 26.12.1 report 1.2.0, and patched 22.23.4 reports 1.1.0, exactly like the vulnerable builds before them. Check the Node.js release itself. If Node links the system Brotli library, check the distribution's brotli package instead.

Has Node2Shell been exploited in the wild?

No confirmed compromise has been reported as of 24 October 2026. CISA's assessment on the CVE record lists exploitation as none, and it is not in the Known Exploited Vulnerabilities catalog. Imperva demonstrated code execution on the official Node.js 24.22.0 Linux arm64 image in a lab, but it took multiple attempts. The first public proof of concept only worked on a deliberately weakened build.

Does a WAF protect against Node2Shell?

Only in one direction. WAF rules from Imperva, Vercel and Indusface block Brotli-encoded request bodies or reproduced attack requests, but no WAF in front of your app can see a malicious response your server fetches from an attacker-controlled URL. Cloudflare's WAF changelog listed no Node2Shell rule as of 22 October. Patching the runtime is the only fix that covers both directions.