DEV Community

Stanley A.
Stanley A.

Posted on Originally published at wardenbit.com

Node.js Brotli RCE (CVE-2026-111497): Your fetch() Decodes Brotli by Default

On 2026-10-21 the Node.js project shipped 22.23.4, 24.22.1 and 26.12.1, fixing ten CVEs across the 22.x, 24.x and 26.x lines plus five dependency updates (c-ares, OpenSSL, SQLite, undici, and the eMudhra intermediate CA). Four of the ten are rated high. One of those four is CVE-2026-111497, an out-of-bounds write in the Brotli decoder bundled in deps/brotli and exposed through node:zlib. The advisory title says plainly that it "permits remote code execution."

The other three highs are worth patching today too — NUL truncation in dns.resolve, and two TLS certificate hostname bypasses — but they are narrow in a way the Brotli bug is not. What makes CVE-2026-111497 the interesting one is who it reaches.

Most people read "Brotli RCE" and think: I don't use Brotli. That assumption is where this gets interesting.

What actually triggers it

The bug is in decompression. A malformed Brotli stream makes the decoder write past the end of the buffer it was handed. The decoder is native code inside your Node process, so that write lands in your process's memory. Worst case with a working exploit, that is code execution at the privileges of the Node process.

So the trigger is not "do I use Brotli." It is: does my process ever decompress Brotli bytes that someone else chose?

For a lot of services, the answer is yes, by default, without anyone configuring anything:

Node built-in fetch() (undici)      br responses      always on, no caller option
axios 1.x                           br responses      on, turn off: decompress: false
node-fetch 3.x                      br responses      on, turn off: compress: false
got 16.x                            br responses      on, turn off: decompress: false
Express 5 parsers (body-parser 2.x) br request bodies on, turn off: inflate: false
Express 4 parsers (body-parser 1.x) unsupported       n/a — br bodies get a 415
Enter fullscreen mode Exit fullscreen mode

fetch() is the one that matters most here, because it has no off switch. It is not a caller option in undici; the only ways out are proxying through your own decompression or replacing the client.

I tested this on v24.22.1 against a local server that answers with a Brotli body no matter what was requested:

SERVER SAW accept-encoding: gzip, deflate
fetch() status: 200
fetch() body: PAYLOAD-DECODED-OK
Enter fullscreen mode Exit fullscreen mode

Read that twice. The client never advertised Brotli, and fetch() decoded it anyway. Whether Node decompresses depends on the response's Content-Encoding header, not on what your request asked for. Over HTTPS the same test shows Accept-Encoding: br, gzip, deflate, but the plain-HTTP case is the one that matters: an attacker who is in the path does not need you to have advertised anything.

The rest of the list above is genuinely configurable. Both axios and body-parser gate decompression on an option that defaults to on, so decompress: false and inflate: false do turn it off. That is a real mitigation on those paths — just be aware of what it costs you in bandwidth and what it does to clients that only send Brotli.

Express 5 decompresses Brotli request bodies by default. If you run an internet-facing API on Express 5 and accept compressed bodies, an attacker posts the payload directly.

Which means the highest-risk workloads are the ones that fetch URLs other people choose: link previews and unfurlers, webhook delivery to customer-configured endpoints, crawlers, feed readers, image proxies, and agent tools that fetch arbitrary pages. On a plain-HTTP path, anyone positioned in the middle can supply a Brotli response to a client that never asked for one.

End-of-life lines get nothing

The fix landed on 22.x, 24.x and 26.x. Everything else is frozen:

26.x  Current, Active LTS from Oct 28   fixed 26.12.1   supported to Apr 30, 2029
24.x  Maintenance LTS                   fixed 24.22.1   supported to Apr 30, 2028
22.x  Maintenance LTS                   fixed 22.23.4   supported to Apr 30, 2027
25.x  EOL since Jun 1, 2026             no fix          last release 25.9.0
23.x  EOL since Jun 1, 2025             no fix          last release 23.11.1
20.x  EOL since Apr 30, 2026            no fix          last release 20.20.2
18.x  EOL since Apr 30, 2025            no fix          last release 18.20.8
Enter fullscreen mode Exit fullscreen mode

If you are on 20 or 18, there is no patch coming. The remediation is a major version upgrade, not a point release.

The fix is a one-line change nobody labelled as security

The flawed code is Google's Brotli decoder, not Node's. The upstream fix is commit 0ed4268, "Detect and report invalid stream earlier," authored at Google on 2026-02-27. The commit message does not mention security.

It tightens validation of block-type switches in c/dec/decode.c, so a malformed stream is rejected with a new error code, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (−17), in a header slot that had been marked reserved. Node cherry-picked it onto all three lines through a private security PR.

That new constant is also the cheapest way to check whether a given binary is patched:

node -p "require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH || 'missing'"
# -17      -> this build includes the fix
# missing  -> it does not
Enter fullscreen mode Exit fullscreen mode

It only reads a constant. Nothing gets decompressed, so it is safe to run anywhere.

Where the version check fails

This is the part that makes the CVE harder to close than it looks, and it is why I would not trust node -v.

Distro packages link system Brotli. Debian, Alpine, Fedora and RHEL build nodejs with --shared-brotli, so the vulnerable code is not Node's bundled copy. The fix has to arrive through the distro's brotli package, and upstream Brotli has not tagged a release containing it. Neither node -v nor the probe above proves those installs are fixed. Only your distro's advisory does.

FROM node:22-alpine is not apk add nodejs. The official image installs Node's own musl build and was rebuilt on October 21. apk add nodejs installs Alpine's package, linked against system Brotli. Same major version, completely different patch channel.

Digest-pinned images do not move. node:22@sha256:... stays exactly where it was until someone updates the digest. And node:20 still pulls happily, last pushed April 2026, and will run the last 20.x build forever.

SEA binaries and bundled runtimes ignore the system. Single executable applications carry their own copy. The answer lives in your build, not on the host.

Version managers keep old runtimes installed. A project's .nvmrc, .node-version, .tool-versions or package.json volta field can silently select an unpatched one per directory. which -a node usually finds more than one, and it is worth running.

PROBE="require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH || 'missing'"
which -a node | while IFS= read -r n; do
  printf '%s  %s  ' "$n" "$("$n" -v)"
  "$n" -p "$PROBE"
done
Enter fullscreen mode Exit fullscreen mode

Containers are the same problem one layer down. A host can run a patched node while every container on it carries a base image from last spring:

for c in $(docker ps --format '{{.Names}}'); do
  V=$(docker exec "$c" node -v 2>/dev/null | head -1)
  case "$V" in
    v*) printf '%-30s %-12s %s\n' "$c" "$V" "$(docker exec "$c" node -p "$PROBE" 2>/dev/null)";;
  esac
done
Enter fullscreen mode Exit fullscreen mode

What the severity actually is

Worth being precise here, because the numbers floating around disagree.

Node.js rates this High and publishes no CVSS score. The MITRE record is still reserved, and NVD has no entry. Some aggregators show 9.8 from a CVSS v3 vector, but that score does not come from the Node.js project or from NVD. I would treat High as the supportable number and ignore the 9.8 until NVD publishes.

On exploitation: the Node.js team does not report in-the-wild activity, and no public proof of concept has been confirmed. It is not in CISA KEV. This is a patch-before-exploitation situation, not an incident.

That said, the exposure surface is genuinely wide. fetch(), axios, node-fetch and Express 5 all hand attacker-supplied bytes to the same decoder without anyone opting in.

What I would do

Patch 26.12.1, 24.22.1 or 22.23.4. If you are on 18 or 20, plan the major upgrade, because no fix is coming for those lines.

Then fix the inventory problem, because updating the node you can see is the easy half. Rebuild container images with docker build --pull so the base actually refreshes. Check digest-pinned images by hand. Check every binary on PATH, not the first one. Check SEA builds against the Node version that produced them. If your distro ships nodejs, wait for the brotli advisory instead of trusting the Node version.

If you are on Express 5 behind a CDN or WAF, note that the WAF covers inbound requests through its network. It does not cover responses your service fetches from elsewhere, and it cannot help with a plain-HTTP fetch that gets a br response injected in transit. The runtime patch is the only thing that covers those paths.


The announcement is at nodejs.org. The CVE record is still reserved at publication time, so the Node advisory is the authoritative source for now.

Top comments (0)