On 21 October 2026 the Node.js project shipped a security release that fixed CVE-2026-111497 - an out-of-bounds write in the Brotli decoder bundled into the runtime, a flaw now known as Node2Shell.[1][2] It matters to anyone who self-hosts: the vulnerable code is not an npm package you can grep for. It lives in the C library that Node, Bun, Python and even proxy runtimes ship by default, and it is reachable over HTTP from both sides of the connection - a malicious client can take your server, and a malicious server can take your client.[3]

Here is the short version, then the details:

  • Affected: Google Brotli versions 1.0.3 through 1.2.0 - which means Node.js on the 22.x / 24.x / 26.x lines before the October fix, Bun ≤ 1.4.3, Python brotli / brotlicffi ≤ 1.2.0, and Envoy / Istio builds that decompress Brotli.[2][4][5]
  • Fixed: Brotli 1.0.10, 1.1.1, 1.2.1; Node.js 22.23.4 / 24.22.1 / 26.12.1; Bun 1.4.4; Python brotli 1.2.1 / brotlicffi 1.2.1.0; Envoy ≥ 1.40.0 / 1.39.3 / 1.38.6 / 1.37.8 / (1.36.12); Istio 1.31.2 / 1.30.6.[1][4][5][6][7][8][9]
  • Severity: 9.2 (Critical) on CVSS 4.0, per the NVD record; the root cause is an unchecked return value (CWE-252) that turns into an out-of-bounds write (CWE-787).[2][10]

What the bug actually is

The flaw lives in c/dec/decode.c of Google’s Brotli C library. Brotli’s decoder is a state machine: a stream is split into metablocks, and inside each metablock the decoder keeps block types for literals, commands and distances, each with its own Huffman tree. A small ring buffer (block_type_rb) tracks recent block-type indices so the decoder can pick the right tree.[3][11]

The specific failure is in DecodeBlockTypeAndLength(). When a block-type switch is attempted but the metablock header declared that no block-type tree exists (max_block_type <= 1), the function correctly returns BROTLI_FALSE. The problem is in the caller: in the fast (non-safe) path, the BROTLI_SAFE macro discards that return value, so the decoder keeps going with stale state. Later it indexes htree_command = s->block_type_rb[3] - an index that is now stale and out of bounds when only one Huffman tree slot actually exists.[11]

That stale, out-of-bounds access is the write that can corrupt adjacent decoder-state memory. Here is Google’s own plain-language description: a remote, unauthenticated attacker can use a crafted Brotli-compressed stream to corrupt adjacent state memory, disclose sensitive memory contents, cause denial of service, or potentially execute arbitrary code.[2]

Google, as the CVE Numbering Authority, scores it 9.2 on CVSS 4.0 - Critical. The Node.js project rates the impact on its own runtime as High in its advisory.[1][2][10] The upstream fix was committed to the Brotli source on 27 February, but no release shipped with it until 21 October - a seven-month window where every consumer of the vulnerable library shipped with the hole open.[3]

Why it is worse than a “Node bug”

The victim is the Brotli decoder, and far more than Node.js bundles one. Here is the affected map, verified against primary advisories on 25 October 2026:

Component Vulnerable Fixed Exposure
Google Brotli (C lib) 1.0.3 – 1.2.0 1.0.10, 1.1.1, 1.2.1 any program calling the decoder[2][9]
Node.js 22.x / 24.x / 26.x before fix 22.23.4, 24.22.1, 26.12.1 node:zlib Brotli APIs; fetch() of Content-Encoding: br; Express/body-parser decoding br request bodies[1][3]
Bun ≤ 1.4.3 1.4.4 (bundles Brotli 1.2.1) fetch() of br response, node:zlib-compatible APIs[6]
Python brotli ≥ 1.0.3, < 1.2.1 1.2.1 request/response decoding by HTTP clients when the package is installed[4]
Python brotlicffi < 1.2.1.0 1.2.1.0 same[4]
Envoy 1.39.2, 1.38.5, 1.37.7, 1.36.11 1.40.0, 1.39.3, 1.38.6, 1.37.8, (1.36.12 unsupported) only builds with the Brotli decompressor configured[5]
Istio those that carry a vulnerable Envoy 1.31.2, 1.30.6 when EnvoyFilter enables Brotli decompression[7][8]

The two-sided attack surface

Brotli reaches the decoder from either direction of an HTTP exchange, and that is what turns a library bug into a broad deployment hazard.[1][3]

Side (a) - the client attacks your server. A Node.js service that decodes a Brotli request body is directly reachable. This happens in two common ways. First, Express 5’s body parsers (express.json(), express.urlencoded(), express.text(), express.raw()) and body-parser 2.x decode a Content-Encoding: br request body when applied to a route. Second, any code that explicitly calls the node:zlib Brotli APIs (zlib.brotliDecompress(), zlib.createBrotliDecompress()) inflates whatever it is given.[1][3] A plain Node HTTP server does not decode a Brotli request body just by receiving one - the decoding requires that middleware or an explicit call - so the exposure depends on what your stack actually inflates.[1]

Side (b) - a malicious server attacks your client. The built-in fetch() in Node and Bun, and curl --compressed, transparently decode a response sent with Content-Encoding: br. That means a service you talk to (an API, a CDN origin, even an ad vendor) can return attacker-crafted Brotli and, if your runtime is unpatched, run code inside your process. This is the side people miss most - you do not have to receive uploads to be exposed; you only have to fetch something untrusted.[1][3]

Imperva’s research team (they named it Node2Shell and published a proof of concept on 22 October) demonstrated actual remote code execution against the official Node.js 24.22.0 Linux arm64 Docker image, with no debugger and no custom build. They delivered a crafted Brotli payload either in a request body processed by Express/body-parser or in a response consumed by fetch(), and a marker file appeared on disk even though the application had no code to create it. The same inputs were rejected by 24.22.1 - no code ran.[3][12][13]

Two honest caveats from Imperva’s write-up: the proof did not succeed on every attempt (execution took multiple tries in their Linux arm64 setup), and they tested that one architecture/build. No in-the-wild exploitation had been reported as of 25 October, and the CVE was not yet in the CISA KEV catalog.[13][14] Neither changes the action: this is a memory-corruption bug in a component that decodes untrusted network input by design, and the fix is a routine but mandatory version bump.[4]

Checking your own VPS

Here is the practical part for a self-hosted box. Run these on every host, then on every container image you build.

Node.js - check the bundled Brotli version:

node -p process.versions.brotli

On an up-to-date runtime you should see 1.2.1 (or higher / a patched branch value). If you are on Node 22.x / 24.x / 26.x and process.versions.brotli reports something in the vulnerable range, or if your Node is below 22.23.4 / 24.22.1 / 26.12.1, you are in scope. Check it inside the image you actually run, not just on the host: docker run --rm <your-image> node -p process.versions.brotli.[1][2]

Bun:

bun --version   # need 1.4.4 or newer
bun -e 'console.log(process.versions.brotli)'

Python:

pip list | grep -i brotli
# or
pip show brotli brotlicffi

You want brotli >= 1.2.1 and brotlicffi >= 1.2.1.0.[4] Here is the quiet danger in Python: the popular HTTP clients - requests, urllib3, httpx, aiohttp - decode Content-Encoding: br responses automatically the moment brotli or brotlicffi is installed, even if your own code never imports them. On many systems one of these packages is pulled in transitively by another dependency, so pip list | grep brotli is the honest check: if either package is present, your clients are decoding Brotli whether your code says so or not. When you upgrade, rebuild your container images - an image built against the vulnerable wheel stays vulnerable until the next build.[4]

Proxy / mesh runtimes: for Envoy, you are exposed only when the Brotli decompressor is actually configured; the advisory fixes 1.36.12, 1.37.8, 1.38.6, 1.39.3, 1.40.0 (1.36 is end-of-life and unsupported).[5] For Istio, the fix landed in 1.31.2 / 1.30.6 and arrives via the Envoy update - upgrade the control plane and let the sidecars roll.[7][8]

What about nginx and your reverse-proxy stack?

Your nginx reverse proxy itself is almost certainly not the vulnerable decoder here. Open-source nginx (and the nginx ngx_brotli module, plus the F5 NGINX Plus build) uses Brotli to compress responses on the fly - the encoder - and to serve pre-compressed files; nginx does not decode a client’s incoming Content-Encoding: br request body by default the way an application layer does.[15][16] So the real decision points on an nginx + Node/Python setup live behind the proxy:

  • The Node/Python application that inflates request bodies (Express/body-parser, Python frameworks) or fetch()es external services - this is where the decoder runs and where you must patch.
  • The clients you run from the box: curl --compressed, Node/Bun scripts that call APIs. On a patched runtime they are safe; on an unpatched one you are exposed to every server you talk to.

The honest framing for the self-hosted crowd: the reverse proxy is not your wall against this CVE. It does not decode request bodies, so blocking br at nginx (by stripping or ignoring Content-Encoding: br before it reaches the app, or restricting Accept-Encoding on responses you proxy) can reduce the surface, but it does nothing for the outbound fetch() side, and the load balancer itself is not the decoder. The fix lives in the runtimes behind it. If you are setting up TLS and proxy headers correctly, that is already in our hardened nginx reverse proxy guide - the runtimes are the missing piece.[17]

For the upgrade mechanics themselves, if you run Node behind PM2 the restart routine is the same one we describe in the Next.js-on-a-VPS deployment guide: update the runtime, rebuild the standalone artifact, restart every PM2 process - a running process holds the old code in memory until you restart it. Out-of-scope EOL lines (Node 20 and earlier) do not get the fix and have no in-runtime workaround, so the only path from those is moving to a supported line.[1][3]

The upgrade order

  1. Node-like runtimes first - anything that decodes untrusted Brotli from the network and holds data you care about: Node to 22.23.4 / 24.22.1 / 26.12.1, Bun to 1.4.4.[1][6]
  2. Python - upgrade brotli >= 1.2.1, brotlicffi >= 1.2.1.0, then rebuild images so the wheels actually change.[4]
  3. Proxy / mesh - Envoy to a fixed release (≥ 1.40.0 / 1.39.3 / 1.38.6 / 1.37.8), Istio to 1.31.2 / 1.30.6, when Brotli decompression is enabled.[5][7][8]
  4. Rebuild every container that bundles a Node/Bun/Python runtime - the binary inside the image is what matters, and a package inventory can miss a Brotli library baked into the layer.[3]
  5. Re-check after restart with the commands above, inside the running container, and confirm process.versions.brotli / pip list look right.

There is no workaround inside Node.js - the advisory lists none, so an application that cannot update has no in-runtime fallback beyond limiting untrusted Brotli traffic on the affected paths until the upgrade lands.[1][12] The fix that closes this for everyone, at every layer, is the same one-word answer: update. Your VPS does not patch itself - that is the job the hosting platform does for someone else, and the one you took on when you self-hosted.

Sources

  1. Node.js - “Wednesday, October 21, 2026 Security Releases” (26.x/24.x/22.x lines; highest severity HIGH; EOL lines always affected by a security release). https://nodejs.org/en/blog/vulnerability/october-2026-security-releases - checked 2026-10-25
  2. GitHub Advisory GHSA-9v8h-qqgm-4m35 - “Brotli: Out-of-bounds write in the Brotli decoder (‘Node2Shell’) also affects the PyPI Brotli package” (Critical, 9.2 CVSS 4.0; affected Google Brotli 1.0.3–1.2.0; patched brotli 1.2.1 / brotlicffi 1.2.1.0, referencing brotli 1.0.10/1.1.1/1.2.1). https://github.com/advisories/GHSA-9v8h-qqgm-4m35 - checked 2026-10-25
  3. Imperva - “Node2Shell: From Brotli Data to Remote Code Execution” (RCE demonstrated on official Node.js 24.22.0 Linux arm64 image; fixes 22.23.4/24.22.1/26.12.1; tested Express 5.0.0–5.3.0 and body-parser 2.0.0–2.3.0; upstream patch google/brotli 0ed4268, source fix 27 Feb, release 21 Oct). https://www.imperva.com/blog/node2shell-from-brotli-data-to-remote-code-execution/ - checked 2026-10-25
  4. GitHub Advisory GHSA-9v8h-qqgm-4m35 (Python packages) - affected brotli ≥1.0.3 <1.2.1 / brotlicffi <1.2.1.0; patched 1.2.1 / 1.2.1.0; HTTP clients (requests/urllib3/httpx/aiohttp) decode br automatically when package present. https://github.com/advisories/GHSA-9v8h-qqgm-4m35 ; https://github.com/pypa/advisory-database/tree/main/vulns/brotli/PYSEC-2026-4322.yaml ; https://github.com/pypa/advisory-database/tree/main/vulns/brotlicffi/PYSEC-2026-4323.yaml - checked 2026-10-25
  5. Envoy - “Out-of-Bounds Write in Brotli Decompressor” GHSA-3wvj-j6qm-62mr (Critical; affected v1.39.2/v1.38.5/v1.37.7/v1.36.11; fixed v1.40.0/v1.39.3/v1.38.6/v1.37.8/v1.36.12; exposure only with Brotli decompressor configured). https://github.com/envoyproxy/envoy/security/advisories/GHSA-3wvj-j6qm-62mr - checked 2026-10-25
  6. Bun - v1.4.4 release (22 Oct 2026; bundles Brotli 1.2.1; security fixes for TLS/WebSocket/database clients/package manager, Brotli included; regression fix vs 1.4.3). https://bun.com/blog/bun-v1.4.4 ; GHSA-663w-cv8g-jj2p https://github.com/oven-sh/bun/security/advisories/GHSA-663w-cv8g-jj2p - checked 2026-10-25
  7. Istio - ISTIO-SECURITY-2026-007 (Envoy CVE-2026-111497; users enabling Brotli decompression via EnvoyFilter must upgrade). https://istio.io/latest/news/security/istio-security-2026-007/ - checked 2026-10-25
  8. Istio - Announcing Istio 1.31.2 (security update including the Envoy Brotli CVE). https://istio.io/latest/news/releases/1.31.x/announcing-1.31.2/ - checked 2026-10-25
  9. Google Brotli releases - v1.0.10 / v1.1.1 / v1.2.1 tags carrying the decoder fix. https://github.com/google/brotli/releases/tag/v1.2.1 - checked 2026-10-25
  10. NVD - CVE-2026-111497 (Google; CVSS 4.0 = 9.2; out-of-bounds write in Brotli decoder; CWE-252 / CWE-787; remote unauthenticated). https://nvd.nist.gov/vuln/detail/CVE-2026-111497 - checked 2026-10-25
  11. Raven - “Node2Shell: Node.js RCE Risk in CVE-2026-111497” (root-cause walkthrough of DecodeBlockTypeAndLength / BROTLI_SAFE / stale block_type_rb[3]; upstream fix commit 0ed4268 changes return to BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH). https://raven.io/blog/node2shell-cve-2026-111497-nodejs-brotli - checked 2026-10-25
  12. The Hacker News - “Researchers Demonstrate Node2Shell RCE Exploit on Node.js Build Using Crafted Brotli Data” (Imperva PoC on Node 24.22.0 arm64; Express 5 body parsers decode br request bodies; no in-runtime workaround listed). https://thehackernews.com/2026/10/researchers-demonstrate-node2shell-rce.html - checked 2026-10-25
  13. The Hacker News - “Brotli Decoder Flaw Behind Node.js RCE Also Affects Bun, Python and Other Widely Used Software” (Envoy/Istio affected table; no in-the-wild exploitation, not in CISA KEV as of ~23-25 Oct; source fix dated 27 Feb). https://thehackernews.com/2026/10/brotli-decoder-flaw-behind-nodejs-rce.html - checked 2026-10-25
  14. Cycode - “node2shell (CVE-2026-111497): What to Patch in Node.js” (as of 23 Oct no in-the-wild exploitation, not in CISA KEV; fix Node 22.23.4/24.22.1/26.12.1, Bun 1.4.4). https://cycode.com/blog/node2shell-cve-2026-111497 - checked 2026-10-25
  15. NGINX - Brotli dynamic module (ngx_http_brotli filter = response compression; static = precompressed files; NGINX Plus). https://docs.nginx.com/nginx/admin-guide/dynamic-modules/brotli/ - checked 2026-10-25
  16. google/ngx_brotli - module set: ngx_http_brotli_filter_module (compress responses) and ngx_http_brotli_static_module (serve pre-compressed); no inbound request-body decompression. https://github.com/google/ngx_brotli - checked 2026-10-25
  17. DeploySmith - hardened nginx reverse proxy guide (TLS, headers, proxy hygiene). https://deploysmith.dev/guides/nginx-reverse-proxy-config/ - checked 2026-10-25