🚀 New: chi (χ) — an open-source autoresearch harness for fleets of LLM coding agents. Read the announcement.

A Brotli Decoder Treated a Bad Block Switch as 'Send More Bytes', and Kept Writing Past an 8-Byte Buffer

CVE-2026-111497 is an unchecked return value in Brotli's streaming decoder: an invalid block switch looked like 'need more input', so the decoder kept appending to a small internal buffer. What the fix changes, and why installing a Python package can switch the exposure on.

Contents

The Brotli bug disclosed on 21 October is not an integer overflow or a bad length field. According to the Node.js advisory, malformed input made the streaming decoder write past the end of an eight-byte internal buffer because “an invalid block switch was reported as a temporary need for more input rather than a permanent format error.” A permanent error should end the stream. A temporary one tells the caller to supply more bytes and call again, so the decoder kept appending whatever it was given.

That detail matters more than the CVSS score, because it tells you where the bug lives (the contract between the decoder’s internals and its caller) and why the exposure is wider than “apps that import brotli”.

What the upstream diff changes

The GitHub advisory GHSA-9v8h-qqgm-4m35 places the flaw in c/dec/decode.c and classifies it as CWE-252, Unchecked Return Value. It lists four upstream fix commits, and I read two of them, 0ed4268 and da98063. I could only read them through a summarizing fetch, not a raw checkout, so I am paraphrasing the change and not quoting code.

Before the fix, the helpers that handle a block switch for literals, commands and distances had inconsistent return types. DecodeBlockTypeAndLength returned a boolean, and the non-safe wrappers (DecodeLiteralBlockSwitch, DecodeCommandBlockSwitch, DecodeDistanceBlockSwitch) returned void, discarding whatever the inner helper reported. One of the failure paths, a tree with one or fewer block types, was simply false, and nothing downstream could distinguish it from “ran out of input”.

After the fix:

  • DecodeBlockTypeAndLength and the wrappers return a BrotliDecoderErrorCode.
  • The one-or-fewer-block-types case returns a new BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH, which takes the previously reserved error slot -17.
  • Running out of input returns BROTLI_DECODER_NEEDS_MORE_INPUT, as before, but now only for that case.
  • A new macro, BROTLI_SAFE_WITH_STATUS, stores any non-success status and jumps to the decoder’s save-state-and-return path. ProcessCommandsInternal uses it at the three block-switch call sites.
  • A debug assertion checks that the remaining meta-block length stays within BROTLI_BLOCK_SIZE_CAP.

The point of a resumable decoder is that “not enough bytes yet” is a normal state and must be cheap to re-enter. That design makes it easy to let a real error fall into the same lane. If you maintain any streaming parser with a need-more-input status, the review question this bug suggests is: can any malformed-input path return the same value as a short read?

Which copies of the decoder you run

The advisory’s affected range is Brotli 1.0.3 through 1.2.0, with fixes in 1.0.10, 1.1.1 and 1.2.1, all tagged on 21 October per the google/brotli releases page. That range is only the start, because the decoder is vendored or bundled in many places. The Hacker News summary lists separate fixed versions for Node.js (22.23.4, 24.22.1, 26.12.1), Bun 1.4.4, Envoy, Istio, the Python Brotli and brotlicffi packages (1.2.1 and 1.2.1.0), and cdxgen. Node.js 20 and older are end of life and get no fix.

Two different exposure descriptions are in circulation, and they are worth keeping apart. Node.js’s own advisory names node:zlib, for example zlib.brotliDecompress on an untrusted buffer. The Hacker News article adds that built-in fetch() reading a Content-Encoding: br response, and Express 5 or body-parser 2 decoding a request body, are also exposed. I did not verify the fetch() and Express claims against a primary source, so treat them as reported.

The Python case is the one most teams would miss. I installed urllib3 2.8.0 in a clean virtual environment and asked it for its default headers:

without Brotli installed: {'accept-encoding': 'gzip,deflate'}
with Brotli 1.2.0:        {'accept-encoding': 'gzip,deflate,br'}

So adding the package, directly or as someone else’s dependency, changes what your HTTP client asks servers for and enables a decode path through the vulnerable C code. Your own code never imports brotli. The urllib3 source I read also treats a missing Brotli package as “no br decoder”, so removing the package turns the path back off. That is consistent with the uninstall workaround the Hacker News summary lists. I did not run the exploit and have no reason to think one is public: the same summary says Imperva’s proof of concept is unreleased and CISA assessed exploitation as “none” as of 23 October.

What the mitigations do and do not cover

The reported workarounds fall into three groups, and they do different things:

WorkaroundCoversDoes not cover
Reject Content-Encoding: br on requestsServer-side decoding of client bodiesResponses your own clients fetch
Disable automatic br decoding in clientsMalicious or compromised upstreamsServer-side request decoding
Uninstall Python Brotli / brotlicffiurllib3-based clients (requests, httpx, aiohttp as reported)Other decoders in your stack

The Hacker News summary also notes that removing br from Accept-Encoding is not enough on its own, since a hostile server does not have to honor what you asked for, and quotes Vercel saying a WAF does not see responses a function fetches from other servers. The direction matters: OpenAI’s description, as relayed there, is that this works both ways, so a malicious server can hit a client and a malicious client can hit a server.

Updating is the only complete fix according to those reports. For a container fleet that means checking each layer separately: the Node.js binary, the Python site-packages, any proxy that ships its own Brotli, and tooling images such as scanners that decompress untrusted input.

A note on the February change

The Hacker News article says the underlying change landed in the Brotli source on 27 February 2026, with no release carrying it and nobody flagging it as a security fix. I could not confirm that date from a primary source. The two fix commits I read have no date in what I retrieved, and the GHSA does not mention it. If it is accurate, it is a useful reminder that “fixed in main” and “fixed in a release you consume” are separate events, and vendored copies pin to the second.

Sources