critical

CVE

CVE-2026-111497, CVE-2026-114148, CVE-2026-115936, CVE-2026-116566, CVE-2026-105482, CVE-2026-105483, CVE-2026-106069, CVE-2026-106072, CVE-2026-105481, CVE-2026-106074, CVE-2026-106070, CVE-2026-106071, CVE-2026-106073

CWE

CWE-787, CWE-170, CWE-295, CWE-297, CWE-330

Affected Surface

  • Node.js 26.x before `26.12.1`, 24.x LTS before `24.22.1`, and 22.x LTS before `22.23.4` (plus end-of-life Node.js 20.x and older lines, which receive no patch)
  • Upstream Google `brotli` (`libbrotli`) before `1.2.1`, `1.1.1`, and `1.0.10`, .NET 8 before `8.0.33`, .NET 9 before `9.0.22`, .NET 10 before `10.0.14`, and .NET 11 before `RC3` (`GHSA-p3jp-9v83-76w9`), PHP 8.2 to 8.5 Windows builds before `8.5.12-1`, `8.4.27-1`, `8.3.36-1`, and `8.2.35-1`, Bun before `1.4.4`, PyPI `Brotli` (`<= 1.2.0`, fixed in `1.2.1`) and `brotlicffi` (`<= 1.2.0.2`, fixed in `1.2.1.0`), Envoy before `1.40.0`, `1.39.3`, `1.38.6`, and `1.37.8`, Istio before `1.31.2` and `1.30.6`, Perl CPAN `IO::Compress::Brotli` (`>= 0.004001, < 0.023`, `CVE-2026-115936`), and native consumers (`git-remote-https` via `libcurl`, FFmpeg via `libjxl` `brob` boxes, ClickHouse, and Apache Arrow `pyarrow`)
  • Node.js servers (including Express 5 and `body-parser` 2.x), proxies, and SSR frameworks that decompress `Content-Encoding: br` request bodies or call `zlib.brotliDecompress*` / `zlib.createBrotliDecompress()`, as well as Node.js, Bun, Envoy, and Python (`urllib3`, `requests`, `httpx[brotli]`, `aiohttp`, `Scrapy`) applications that fetch Brotli-compressed responses from untrusted remote URLs
  • Applications using `dns.resolve*()` after suffix-based hostname validation, `tls.connect()` / `https.request()` with user-influenced hostnames, or V8 startup snapshots (`--build-snapshot`) and Single Executable Applications (SEAs)

Update, 29 October 2026: On 29 October (17:46 UTC), the Python Security Response Team announced CVE-2026-116566 (oss-security/2026/10/29/4, python/cpython#160898, PR #160899) for the exact CPython counterpart of Node.js CVE-2026-105482: socket.getaddrinfo() previously accepted embedded null bytes (\x00) inside both the host and port parameters, allowing C string truncation inside the underlying OS resolver to resolve "attacker.example\x00.trusted.corp" after passing Python-level suffix checks.

Update, 28 October 2026: On 27 October 2026, CISA added CVE-2026-111497 (Google Brotli Unchecked Return Value Vulnerability) to its Known Exploited Vulnerabilities (KEV) Catalog alongside Adobe Commerce CVE-2026-48356, citing evidence of active exploitation in the wild and setting a federal remediation due date of 10 November 2026 under BOD 26-04. FIRST EPSS for CVE-2026-111497 has reached 55.48% (99.03rd percentile). In addition, Go’s pure-Go Brotli port github.com/andybalholm/brotli shipped v1.2.7 (commit 33c2a98842) to reject streams with invalid block-switch types early, and Wiz Research reported that 91% of cloud environments carry resources vulnerable to CVE-2026-111497.

Update, 26 October 2026: Between 25 and 26 October, CVE-2026-111497 (also tracked as Brotli2Shell / Broccoli2Shell) triggered emergency out-of-band releases across .NET and PHP Windows builds and was demonstrated in non-HTTP file parsers:

  • Microsoft .NET out-of-band updates (8.0.33, 9.0.22, 10.0.14, and 11 RC3, GHSA-p3jp-9v83-76w9): On 25 October, Microsoft released emergency updates for .NET 8, 9, 10, and 11 RC3 because the native Brotli decoder compiled into the .NET runtime (System.IO.Compression.Brotli, BrotliStream, BrotliDecoder, ASP.NET Core AddRequestDecompression, and HttpClient AutomaticDecompression = DecompressionMethods.Brotli) carried the vulnerable DecodeBlockTypeAndLength() path. The same out-of-band release also patches CVE-2026-114148 (unsafe Linux .tar.gz ownership metadata) and re-releases complete fixes for CVE-2026-70197 and CVE-2026-102870. Self-contained .NET deployments and container images must be rebuilt and redeployed.
  • PHP for Windows -1 rebuilds (8.5.12-1, 8.4.27-1, 8.3.36-1, and 8.2.35-1): On 26 October, the PHP project rebuilt its official Windows zip distributions after discovering that the 21 October Windows releases (8.5.12 through 8.2.35) bundled brotli 1.2.0 inside php_curl.dll (reachable whenever CURLOPT_ENCODING is set to "" or "br", which PHP’s Guzzle HTTP client enables by default). Because php -v still prints 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35, Windows administrators must verify the -1 archive filename (php-8.5.12-1-nts-Win32-vs17-x64.zip) or check the libbrotlidec.dll / php_curl.dll SHA-256 digest.
  • Non-HTTP and native file-parser vectors (FFmpeg libjxl, ClickHouse, Apache Arrow, and git-remote-https): On 25 October, JFrog Security Research demonstrated instruction-pointer control (0x414141414141 crash on decoder teardown) in FFmpeg via a crafted JPEG XL (.jxl) image whose brob Exif metadata box feeds 200010414141fcffff07002228f00b60f7f41f000000 into libjxl -> BrotliDecoderDecompressStream(), a single-request SIGSEGV crash in ClickHouse via Content-Encoding: br (fixed in ClickHouse/ClickHouse#127419), and an infinite CPU loop in Apache Arrow (pyarrow) via a 534-byte Brotli-compressed Parquet page (apache/arrow#55166). On 26 October, Harry Sintonen noted on oss-security (2026/10/26/16) that git-remote-https (git clone https://...) sets CURLOPT_ACCEPT_ENCODING (http.c:2284) and therefore exposes git clients whenever system libcurl is linked against an unpatched libbrotlidec.

Update, 24 October 2026: On 23 and 24 October, the downstream blast radius of CVE-2026-111497 (now tracked across vendors as HTTP2Shell) widened beyond Node.js, Bun, and Python:

  • Envoy and Istio: Envoy published GHSA-3wvj-j6qm-62mr and shipped 1.40.0, 1.39.3, 1.38.6, and 1.37.8 (plus 1.36.12 on the unsupported 1.36 branch), while Istio shipped 1.31.2 and 1.30.6 (plus end-of-life 1.29.9) to update the bundled libbrotli used by Envoy’s Brotli HTTP decompressor filter.
  • Perl CPAN (CVE-2026-115936): The CPAN Security Group assigned CVE-2026-115936 (oss-security/2026/10/23/31) to IO::Compress::Brotli versions 0.004001 before 0.023. Because IO::Compress::Brotli compiles its own bundled C brotli submodule (1.0.4 through 1.2.0) rather than linking the OS library, upgrading system libbrotli does not patch Perl callers using unbro or decompress; upgrade to IO-Compress-Brotli 0.023 (commit 39a62a4454).
  • Python advisory mappings: On 24 October, the PyPI security team and GitHub Advisory Database published PYSEC-2026-4322 (Brotli <= 1.2.0), PYSEC-2026-4323 (brotlicffi <= 1.2.0.2), and updated GHSA-9v8h-qqgm-4m35, allowing pip-audit and Dependabot to flag vulnerable Python environments automatically.
  • Origin of the February 2026 commit: OpenAI disclosed that its researchers reported the out-of-bounds write to Google in February 2026 as a bidirectional RCE vector affecting both servers and HTTP clients. Although commit 0ed4268a4401 landed in google/brotli master in February 2026, no upstream release was tagged until v1.2.1, v1.1.1, and v1.0.10 on 21 October 2026.

Update, 23 October 2026: On 22 October, Imperva Red Team published its Node2Shell research demonstrating remote code execution against the official, unmodified node:24.22.0 Linux arm64 container image via both inbound Content-Encoding: br request bodies (express@5.0.0 to 5.3.0 and body-parser@2.0.0 to 2.3.0, which automatically inflate Brotli bodies in Express 5) and outbound fetch() responses. Google CNA and NVD also scored CVE-2026-111497 at CVSS v4.0 9.2 (Critical). Because the flaw lives in Google’s C brotli decoder (c/dec/decode.c), it extends beyond Node.js: Bun shipped v1.4.4 on 22 October (oven-sh/bun#46352), and on 22 and 23 October the maintainers of the PyPI Brotli (<= 1.2.0 -> 1.2.1) and brotlicffi (<= 1.2.0.2 -> 1.2.1.0) packages quietly published updated wheels compiling the patched C source without release notes or PyPA/GHSA advisory records. Any Python environment with Brotli or brotlicffi installed causes urllib3, requests, httpx, aiohttp, and Scrapy to advertise Accept-Encoding: br and feed remote HTTP responses into the same C decoder.

TL;DR

On 21 October 2026 (followed by oss-security and container image rollouts on 22 October), the Node.js project released v26.12.1 (Current), v24.22.1 (Active LTS), and v22.23.4 (Maintenance LTS) to fix ten security vulnerabilities alongside the eMudhra intermediate CA public-key blocklist. Google tagged upstream brotli v1.2.1, v1.1.1, and v1.0.10 the same day.

Four of the ten Node.js CVEs are rated High severity, and two Medium-severity fixes close subtle runtime primitives:

  1. CVE-2026-111497 (High): An out-of-bounds write in the C Brotli decoder (deps/brotli/c/dec/decode.c) permits remote code execution when decompressing crafted Brotli streams. On the fast (“unsafe”) decode path used when sufficient input bytes are buffered, BROTLI_SAFE() ignored the return value of DecodeCommandBlockSwitch(), DecodeDistanceBlockSwitch(), and DecodeLiteralBlockSwitch(), allowing decoding to continue with corrupted block-switch state when max_block_type <= 1. This is reachable both on inbound Content-Encoding: br requests and on outbound fetch() calls, because Node’s built-in fetch() (Undici) automatically decodes Brotli responses from remote servers.
  2. CVE-2026-105482 (High): dns.resolve*() and resolver.setLocalAddress() validated inputs with validateString() instead of validateStringWithoutNullBytes(). Because c-ares treats the hostname as a NUL-terminated C string, passing 'attacker.example\0.trusted.corp' passes a JavaScript .endsWith('.trusted.corp') check and then resolves attacker.example.
  3. CVE-2026-106069 and CVE-2026-105483 (High, plus CVE-2026-106072 Medium): tls.checkServerIdentity() normalized hostnames with url.domainToASCII() in ways that disagreed with net.isIP() and OS resolvers. Fullwidth IPv4 digits (\uFF11\uFF12\uFF17.0.0.1), ideographic dots (127\u30020.0.1), and non-canonical IPv4 strings (0127.0.0.1, 0x7f.0.0.1, 2130706433, 127.1) either fell through to DNS-name / Common Name matching or validated the certificate against 87.0.0.1 (127 in octal) while the OS connected to 127.0.0.1.
  4. CVE-2026-106074 (Medium): When building a V8 startup snapshot (--build-snapshot) or Single Executable Application (SEA), any call to crypto.randomUUID(), crypto.randomUUIDv7(), or crypto.randomInt() during snapshot creation left pre-generated entropy batches inside randomCache and uuidData. Every process spawned from that snapshot or SEA binary replayed the exact same UUIDs and random integers.

Upgrade to Node.js 26.12.1, 24.22.1, or 22.23.4 (and migrate off end-of-life Node.js 20.x). Because Node cherry-picked the Brotli patch without bumping process.versions.brotli, verify process.versions.node rather than the embedded Brotli version string.

Affected packages and projects

EcosystemPackage / runtimeCVESeverityAffected versionsFixed versionsComponent
Node.jsnodeCVE-2026-111497High (CVSS v4.0 9.2 Critical)22.x < 22.23.4, 24.x < 24.22.1, 26.x < 26.12.1, <= 20.x (EOL)22.23.4, 24.22.1, 26.12.1deps/brotli/c/dec/decode.c (zlib Brotli decompression, Express 5 / body-parser 2.x, and fetch())
C / C++ / distrosgoogle/brotli (libbrotli)CVE-2026-111497CVSS v4.0 9.2 Critical< 1.0.10, 1.1.0, 1.2.01.0.10, 1.1.1, 1.2.1c/dec/decode.c (DecodeBlockTypeAndLength / ProcessCommandsInternal)
BunbunCVE-2026-111497CVSS v4.0 9.2 Critical< 1.4.4 (<= 1.4.2 bundles brotli 1.1.0; 1.4.3 bundles 1.2.0)1.4.4Vendored brotli C decoder (fetch(), node:zlib, DecompressionStream("brotli"))
PyPIBrotliCVE-2026-111497 (PYSEC-2026-4322)CVSS v4.0 9.2 Critical<= 1.2.01.2.1CPython C extension compiled from c/dec/decode.c (auto-used by urllib3, requests, httpx, aiohttp)
PyPIbrotlicffiCVE-2026-111497 (PYSEC-2026-4323)CVSS v4.0 9.2 Critical<= 1.2.0.21.2.1.0CFFI binding compiled from vendored c/dec/decode.c
Envoy / Istioenvoy, istioCVE-2026-111497 (GHSA-3wvj-j6qm-62mr)CVSS v4.0 9.2 CriticalEnvoy < 1.37.8, 1.38.x < 1.38.6, 1.39.x < 1.39.3, 1.40.x < 1.40.0; Istio < 1.30.6, 1.31.x < 1.31.2Envoy 1.37.8, 1.38.6, 1.39.3, 1.40.0; Istio 1.30.6, 1.31.2Envoy Brotli HTTP decompressor filter (libbrotli)
.NET / NuGetMicrosoft.NETCore.App, Microsoft.DotNet.ILCompilerCVE-2026-111497 (GHSA-p3jp-9v83-76w9)CVSS 8.1 High8.0.x < 8.0.33, 9.0.x < 9.0.22, 10.0.x < 10.0.14, 11.0 < RC38.0.33, 9.0.22, 10.0.14, 11.0.0-rc.3Native libbrotli compiled into .NET runtime (BrotliStream, BrotliDecoder, HttpClient, ASP.NET Core AddRequestDecompression)
PHP (Windows)php Windows x64/x86 buildsCVE-2026-111497CVSS v4.0 9.2 Critical8.2 <= 8.2.35, 8.3 <= 8.3.36, 8.4 <= 8.4.27, 8.5 <= 8.5.128.2.35-1, 8.3.36-1, 8.4.27-1, 8.5.12-1Bundled brotli 1.2.0 in Windows php_curl.dll (CURLOPT_ENCODING / Guzzle)
CPAN (Perl)IO::Compress::BrotliCVE-2026-115936High>= 0.004001, < 0.0230.023Bundled brotli submodule (1.0.4..1.2.0) compiled into module (unbro / decompress)
Node.jsnodeCVE-2026-105482High22.x < 22.23.4, 24.x < 24.22.1, 26.x < 26.12.122.23.4, 24.22.1, 26.12.1lib/internal/dns/* (dns.resolve*, Resolver.setLocalAddress)
Node.jsnodeCVE-2026-105483High24.x < 24.22.1, 26.x < 26.12.124.22.1, 26.12.1lib/tls.js (checkServerIdentity fullwidth/ideographic IP regression)
Node.jsnodeCVE-2026-106069, CVE-2026-106072High / Medium22.x < 22.23.4, 24.x < 24.22.1, 26.x < 26.12.122.23.4, 24.22.1, 26.12.1lib/tls.js (checkServerIdentity non-canonical IPv4 parsing)
Node.jsnodeCVE-2026-106074Medium22.x < 22.23.4, 24.x < 24.22.1, 26.x < 26.12.122.23.4, 24.22.1, 26.12.1lib/internal/crypto/random.js (V8 startup snapshots and SEAs)
Node.jsnodeCVE-2026-105481Medium22.x < 22.23.4, 24.x < 24.22.1, 26.x < 26.12.122.23.4, 24.22.1, 26.12.1lib/_http_agent.js (keep-alive socket processing pre-request data)
Node.jsnodeCVE-2026-106070, 106071, 106073Low26.x < 26.12.1 (106073 also 22.x, 24.x)22.23.4, 24.22.1, 26.12.1zlib ZipFile header cache and --permission radix/realpath checks

Technical analysis

1. CVE-2026-111497: ignored block-switch errors on Brotli’s fast decode path

Reported by Khai Tran and Sylvie Mayer (OpenAI Codex Security), CVE-2026-111497 sits in deps/brotli/c/dec/decode.c (upstream commit 0ed4268a4401abcb4472096340477cf4af7d1a2b, cherry-picked into Node.js in commit a6040b50d9).

Brotli’s command loop ProcessCommandsInternal(int safe, BrotliDecoderState* s) is compiled twice: once with safe = 1 (used near the end of an input chunk when the bit reader might run out of bytes) and once with safe = 0 (the fast path used when at least 28 bytes of input remain buffered). Before the fix, both paths wrapped helper calls in the BROTLI_SAFE(METHOD) macro:

/* NB: METHOD should return BROTLI_FALSE only in case there is not enough input;
       in case of "unsafe" execution, when input is guaranteed to be sufficient,
       result is ignored. */
#define BROTLI_SAFE(METHOD)                       \
  {                                               \
    if (safe) {                                   \
      if (!Safe##METHOD) {                        \
        result = BROTLI_DECODER_NEEDS_MORE_INPUT; \
        goto saveStateAndReturn;                  \
      }                                           \
    } else {                                      \
      METHOD;                                     \
    }                                             \
  }

Look at the comment and the else branch: when safe == 0, BROTLI_SAFE() assumes METHOD cannot fail for any reason other than running out of input, so it calls METHOD as a void statement and discards the result.

However, the three block-switch helpers (DecodeLiteralBlockSwitch, DecodeCommandBlockSwitch, and DecodeDistanceBlockSwitch) call DecodeBlockTypeAndLength(safe, s, tree_type), which can fail on a format error even when plenty of input bytes are buffered:

static BROTLI_INLINE BROTLI_BOOL DecodeBlockTypeAndLength(
    int safe, BrotliDecoderState* s, int tree_type) {
  brotli_reg_t max_block_type = s->num_block_types[tree_type];
  ...
  if (max_block_type <= 1) {
    return BROTLI_FALSE;
  }

When a crafted stream causes s->block_length[tree_type] to hit 0 while max_block_type <= 1:

  1. On the safe == 1 path, DecodeBlockTypeAndLength() returned BROTLI_FALSE, which BROTLI_SAFE() misinterpreted as BROTLI_DECODER_NEEDS_MORE_INPUT instead of a fatal stream error.
  2. On the safe == 0 fast path, DecodeCommandBlockSwitch(s), DecodeDistanceBlockSwitch(s), and DecodeLiteralBlockSwitch(s) returned void, silently ignoring the BROTLI_FALSE return value without updating s->block_length[tree_type], s->htree_command, or s->dist_context_map_slice. Execution then jumped straight back into CommandBegin or CommandInner with desynchronized Huffman table pointers and block lengths, leading to an out-of-bounds write.

The patch introduces a dedicated error code BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (-17), changes DecodeBlockTypeAndLength() and all three *BlockSwitch* functions to return BrotliDecoderErrorCode, and replaces BROTLI_SAFE at those three call sites with BROTLI_SAFE_WITH_STATUS:

--- a/deps/brotli/c/dec/decode.c
+++ b/deps/brotli/c/dec/decode.c
@@ -1187,7 +1187,7 @@ static BROTLI_INLINE BrotliDecoderErrorCode DecodeBlockTypeAndLength(
   brotli_reg_t* ringbuffer = &s->block_type_rb[tree_type * 2];
   brotli_reg_t block_type;
   if (max_block_type <= 1) {
-    return BROTLI_FALSE;
+    return BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH;
   }
...
+#define BROTLI_SAFE_WITH_STATUS(METHOD)         \
+  {                                             \
+    BrotliDecoderErrorCode status;              \
+    if (safe) {                                 \
+      status = Safe##METHOD;                    \
+    } else {                                    \
+      status = METHOD;                          \
+    }                                           \
+    if (status != BROTLI_DECODER_SUCCESS) {     \
+      result = status;                          \
+      goto saveStateAndReturn;                  \
+    }                                           \
+  }

2. CVE-2026-105482: dns.resolve*() NUL-byte truncation in c-ares

In Node.js, dns.lookup() already rejected strings containing NUL bytes (\0), but the c-ares-backed resolver methods (dns.resolve(), dns.resolve4(), dns.resolve6(), dns.resolveTxt(), dns.resolveSrv(), and their dns.promises equivalents) and resolver.setLocalAddress() only called validateString(name, 'name').

When JavaScript passed 'evil.example\0.trusted.corp' into the C++ QueryReqWrap binding, c-ares read the buffer as a NUL-terminated C string and queried the DNS server for evil.example. Any SSRF filter or domain allowlist that validated hostname.endsWith('.trusted.corp') before calling dns.resolve4(hostname) could be bypassed. Commit 6b78ea171d replaces validateString with validateStringWithoutNullBytes across lib/internal/dns/callback_resolver.js, lib/internal/dns/promises.js, and lib/internal/dns/utils.js.

3. CVE-2026-106069, CVE-2026-105483, and CVE-2026-106072: non-canonical IPv4 and fullwidth digit mismatches in tls.checkServerIdentity()

Per RFC 6125 and RFC 2818, when a TLS client connects to an IP address, the server certificate must be matched against IP Address: Subject Alternative Names (SANs), never against DNS: SANs or the subject Common Name (CN).

Two bugs in lib/tls.js broke that invariant:

  1. Fullwidth digits and ideographic dots (CVE-2026-105483, affecting 24.x and 26.x): An earlier IPv6 fix changed checkServerIdentity() to test if (net.isIP(hostname)) on the raw hostname before IDNA normalization (url.domainToASCII(hostname)). Because net.isIP('\uFF11\uFF12\uFF17.0.0.1') and net.isIP('127\u30020.0.1') return 0, checkServerIdentity() treated them as DNS names, normalized them via domainToASCII() into '127.0.0.1', and matched them against DNS:127.0.0.1, DNS:*.0.0.1, or CN=127.0.0.1.
  2. Non-canonical octal, hex, and integer IPv4 hosts (CVE-2026-106069 and CVE-2026-106072, affecting 22.x, 24.x, and 26.x): WHATWG url.domainToASCII('0127.0.0.1') parses the leading 0127 as octal and returns '87.0.0.1', whereas many OS resolvers (or custom lookup hooks) treat 0127.0.0.1 as decimal 127.0.0.1 (or vice versa for 0177.0.0.1, 0x7f.0.0.1, 2130706433, and 127.1). When !net.isIP(hostname) was paired with domainToASCII(hostname), Node validated the peer’s certificate against the domainToASCII() IP or DNS SAN (87.0.0.1) while the socket connected to a different IP (127.0.0.1).

Commit cd4b05469c and commit ad788ccd07 resolve both issues by rejecting any hostname that is not already a canonical IP literal (net.isIP(hostname) === 0) yet becomes an IP literal after domainToASCII() (net.isIP(hostnameASCIIWithoutFQDN) !== 0):

--- a/lib/tls.js
+++ b/lib/tls.js
@@ -439,6 +439,8 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
   hostname = unfqdn(hostname);
   const hostnameASCIIWithoutFQDN = unfqdn(hostnameASCII);
+  const hostnameIsIP = net.isIP(hostname);
+  const hostnameASCIIIsIP = net.isIP(hostnameASCIIWithoutFQDN);
...
+  // Non-canonical IP address spellings can be interpreted differently by
+  // domainToASCII() and the operating system resolver. Reject them instead of
+  // validating a certificate against an address that may not be the peer.
+  if (!hostnameIsIP && hostnameASCIIIsIP) {
+    return new ERR_TLS_CERT_ALTNAME_INVALID(
+      'Host is a non-canonical IP address', hostname, cert);
+  }

4. CVE-2026-106074: crypto.randomUUID() and randomInt() cache replay in startup snapshots and SEAs

To reduce CSPRNG syscall overhead, lib/internal/crypto/random.js pre-generates random bytes in module-scoped TypedArray buffers (randomCache for randomInt() and uuidData for buffered randomUUID() / randomUUIDv7()). When a developer builds a V8 startup snapshot (node --build-snapshot) or a Single Executable Application (SEA) and any code calls randomUUID() or randomInt() during the snapshot build, the remaining pre-generated bytes in uuidData and randomCache are serialized into the snapshot blob. Every process later launched from that snapshot starts with the same uuidBatch counter and emits identical UUIDs and random integers.

Commit acffa610db registers resetRandomCaches() both after user serialization callbacks (addAfterUserSerializeCallback) and before user deserialization callbacks (addBeforeUserDeserializeCallback), zeroing randomCache, uuidData, and uuidNotBuffered so even caches refilled by process.nextTick or exit handlers during snapshot teardown are cleared before the new process runs.

Detection and remediation

Because Node.js cherry-picked the Brotli patch without changing process.versions.brotli, check process.versions.node and inspect whether your Node binary is statically or dynamically linked against libbrotlidec:

# Check running Node.js version and shared library configuration
node -e 'console.log({ node: process.versions.node, brotli: process.versions.brotli, sharedBrotli: Boolean(process.config.variables.node_shared_brotli) })'

# On Linux, check if the node binary links dynamically against system libbrotlidec
ldd "$(command -v node)" 2>/dev/null | grep -i brotli || true

# Search Dockerfiles, CI workflows, and version files for unpatched Node 22, 24, 26, or EOL Node 20
rg -n 'FROM\s+(\S+/)?node:|node-version:\s*["'\'']?(20|22|24|26)' \
  Dockerfile* .github/workflows .nvmrc .node-version package.json 2>/dev/null

# Check Bun runtime and Python Brotli / brotlicffi packages (which are not flagged by pip-audit yet)
bun --version 2>/dev/null || true
pip show Brotli brotlicffi 2>/dev/null || true
rg -n -i '"?(brotli|brotlicffi)"?\s*([~=!<>]=|:)' \
  requirements*.txt pyproject.toml uv.lock poetry.lock Pipfile.lock 2>/dev/null

2. Upgrade runtimes, Python wheels, and SEA binaries

  1. Upgrade Node.js to 26.12.1, 24.22.1, or 22.23.4, Bun to 1.4.4+, and PyPI Brotli / brotlicffi:
    • If you use official Docker images (node:24-slim, node:24-alpine, node:22-bookworm, node:26), pull the images pushed on or after 2026-10-21T17:38Z (-slim) or 2026-10-22T02:39Z (full Debian and Alpine tags) and redeploy.
    • If your Linux distribution builds nodejs with node_shared_brotli: true against the system libbrotli package, also update libbrotli1 / brotli to 1.2.1, 1.1.1, or the distribution’s backported package for CVE-2026-111497.
    • For Bun workloads, upgrade to bun >= 1.4.4 (bun upgrade).
    • For Python workloads where Brotli or brotlicffi is present, upgrade to Brotli>=1.2.1 and brotlicffi>=1.2.1.0 (pip install -U "Brotli>=1.2.1" "brotlicffi>=1.2.1.0").
    • If a Node.js service using Express 5 (express.json(), express.urlencoded(), express.raw(), express.text()) or body-parser 2.x cannot be upgraded immediately, drop incoming requests carrying Content-Encoding: br at your reverse proxy or load balancer as a temporary inbound stopgap.
    • Migrate any workload still on Node.js 20.x (end-of-life since 30 April 2026) to 22.23.4 or 24.22.1.
  2. Rebuild V8 startup snapshots and Single Executable Applications (SEAs): Any SEA binary or --snapshot-blob built with an older Node.js release embeds both the vulnerable libbrotli machine code (CVE-2026-111497) and potentially primed randomUUID() / randomInt() entropy buffers (CVE-2026-106074). Rebuild them with a patched Node binary and rotate any tokens, session IDs, or keys that were generated at startup by a snapshot-backed binary.

Timeline

  • 2026-10-14: The Node.js project posts a pre-alert announcing that security releases for the 22.x, 24.x, and 26.x lines will ship on or shortly after 21 October 2026.
  • 2026-10-21 (10:38 UTC): Node.js publishes v22.23.4, v24.22.1, and v26.12.1 on GitHub and nodejs.org, disclosing ten CVEs (CVE-2026-111497, CVE-2026-105482, CVE-2026-105483, CVE-2026-106069, CVE-2026-105481, CVE-2026-106072, CVE-2026-106074, CVE-2026-106070, CVE-2026-106071, and CVE-2026-106073).
  • 2026-10-21 (19:26 to 19:27 UTC): Google tags brotli v1.0.10, v1.1.1, and v1.2.1 with the security note for CVE-2026-111497.
  • 2026-10-21 (17:38 UTC) to 2026-10-22 (02:41 UTC): Docker Official Images publish updated library/node tags for 22.23.4, 24.22.1, and 26.12.1.
  • 2026-10-22 (10:11 UTC): Bun releases v1.4.4 with the backported CVE-2026-111497 patch and Rust BrotliDecoderErrorCode2 -17 enum update (oven-sh/bun#46352).
  • 2026-10-22 (13:57 and 16:19 UTC): Brotli 1.2.1 and brotlicffi 1.2.1.0 are uploaded to PyPI, and Imperva Red Team publishes its Node2Shell write-up demonstrating RCE on the official node:24.22.0 Linux arm64 image.
  • 2026-10-22 (18:45 UTC): The full Node.js release summary is posted to oss-security (2026/10/22/8).
  • 2026-10-23 (05:31 UTC): OpenSourceMalware publishes sdist diff and AddressSanitizer verification confirming that PyPI Brotli <= 1.2.0 and brotlicffi <= 1.2.0.2 carried the same vulnerable C decoder without a PyPA or GHSA advisory.

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.

References