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

URL: https://www.ionsec.io/resources/node2shell-brotli-decoder-rce-cve-2026-111497
Category: Article
Author: IONSEC Team
Published: 2026-10-24
Tags: research, threat-intelligence, nodejs, web-security, rce, incident-response, cve-2026-111497, brotli

> Node2Shell, CVE-2026-111497, is an out-of-bounds write in the Google Brotli decoder that Node.js bundles. Malformed Brotli data that an application decompresses can corrupt process memory: a request body that Express 5 or Koa inflates, or a response that fetch() decodes. Node.js fixed it on 21 October 2026 in 22.23.4, 24.22.1 and 26.12.1.

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.

---
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](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases) 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](https://www.imperva.com/blog/node2shell-from-brotli-data-to-remote-code-execution/)'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](/resources/php2shell-two-php-core-rce-vulnerabilities), 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 / line | Vulnerable through | Fixed release | Notes |
| --- | --- | --- | --- |
| Node.js 26 | 26.12.0 | 26.12.1 | |
| Node.js 24 | 24.22.0 | 24.22.1 | Imperva's code-execution target |
| Node.js 22 | 22.23.3 | 22.23.4 | |
| Node.js 25, 23, 20 and older | end of life | no fix | Every EOL line we checked, back to Node 12, carries the vulnerable early return. Node 20 left support on 30 April 2026 |
| Bun | 1.4.3 | 1.4.4 | `fetch()` asks for and decodes Brotli by default, with no verified switch |
| Brotli library | 1.0.3–1.0.9, 1.1.0, 1.2.0 | 1.0.10, 1.1.1, 1.2.1 | The 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](https://security-tracker.debian.org/tracker/CVE-2026-111497) 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:

| Stack | Decodes 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` ≥5 | **Yes, 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/v1` | **Yes.** 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-js | No |
| A bare `node:http` server | No. 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`:

```diff
   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](https://hadrian.io/vulnerability-alerts/node2shell-what-we-know-about-the-node-js-brotli-rce-cve-2026-111497) 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:

| Component | Exposure | Fixed in |
| --- | --- | --- |
| Bun | `fetch()` sends `Accept-Encoding: gzip, deflate, br, zstd` and decodes; also `node:zlib` and `DecompressionStream` | 1.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 `brotlicffi` | 1.0.9.0 through 1.2.0.2 bundle affected decoders | 1.2.1.0 |
| aiohttp servers | **Decode `Content-Encoding: br` request bodies by default** when Brotli is installed. It is the only Python server stack in the survey that does | Upgrade Brotli |
| requests, httpx, urllib3, pip, Scrapy | Decode Brotli responses automatically whenever a Brotli module is importable; Scrapy always does | Upgrade 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-62mr | 1.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.6 | v1.2.7 |
| RubyGems `brotli` / Maven `brotli4j` | 0.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 calls | **No fixed release found** |
| Rust `brotli-decompressor` | The same early return, still present in 6.1.1 | None. 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.0 | Not in the public build as of 24 October. Hosted status unpublished |
| Electron | The Node side of 45.0.0, 44.9.3, 43.8.3 and 42.11.18 predates the fix | Only 46.0.0-alpha.1 carries Node 24.22.1 |
| Deno | Rust decoders, not the C library | Unresolved, not cleared |
| nginx `ngx_brotli`, Apache `mod_brotli` | Encoder only | Not affected |

The Python row deserves its own sentence. [GHSA-9v8h-qqgm-4m35](https://github.com/advisories/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](https://www.imperva.com/blog/node2shell-from-brotli-data-to-remote-code-execution/) 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](https://advisories.ncsc.nl/2026/ncsc-2026-0453.html) 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](https://vercel.com/kb/bulletin/node-js-security-advisory) 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`:

```bash
# 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
# nginx: refuse Brotli-encoded request bodies
if ($http_content_encoding ~* "(^|,)\s*br\s*(,|$)") { return 415; }
```

```js
// 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](https://github.com/ionsec/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>
