Brotli · CVE-2026-111497 · Updated

CVE-2026-111497 (Brotli): npm audit is clean, but the scanner still flags your image

The Brotli bug fixed on 21 October is usually not in a package your lockfile manages. It is in copies of Google's Brotli decoder compiled into other things: the Node.js binary, the OS library (libbrotli1 on Debian and Ubuntu, brotli-libs on Alpine), Python wheels, the .NET runtime, Envoy, and the Go port github.com/andybalholm/brotli. npm audit and pip-audit read your dependency lists, so they can be clean while the image still holds an unfixed copy. As of 3 November 2026, 14:45 UTC, three things decide whether yours is fixed:

  • No Node 20 release has the fix. Node 20 reached end of life on 30 April 2026. The Node project fixed 22, 24 and 26 only: 22.23.4, 24.22.1 and 26.12.1 or later. The last Node 20 release is 20.20.2, from 24 March. The official node:20 image was last pushed on 22 April, and the Node binary in node:20-slim holds the unfixed decoder (we checked, below). Debian's own Node package is different: it uses the system Brotli library (below).
  • A rebuild fixes only the copies it replaces. An image keeps the copies it was built with. node:24.22.1, published with the fixed Node, still has Debian 12's unfixed libbrotli1 1.0.9-2+b6 next to it.
  • The version number doesn't tell you. Fixed Node 24.22.1 still reports Brotli 1.2.0 in process.versions.brotli, the same as unfixed 24.22.0. Debian's fixed libbrotli1 1.1.0-2+deb13u1 still reports 1.1.0. A scanner that matches version strings can flag a fixed copy, and a version check by hand can pass an unfixed one.

Check which copy is flagged

Node.js from nodejs.org or the official Docker images. These carry their own copy of Brotli. This prints true if the Node you run has the fix:

node -p '"BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH" in zlib.constants'

For the Node inside an image, put your image name in place of node:20-slim:

docker run --rm --entrypoint node node:20-slim -p '"BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH" in zlib.constants'

The fix added a new error code, BLOCK_SWITCH, so its name is the marker. A Node.js contributor suggested this test when asked why 24.22.1 still reports Brotli 1.2.0. In our runs on 3 November it printed false for node:20-slim (20.20.2) and node:24.22.0-slim, and true for node:22.23.4, node:24-slim (24.22.1) and node:26-slim. We ran it only on the official images. Don't use it on Debian's packaged Node: that Node uses the system Brotli library, and in our run on Debian 13 it printed false even with the fixed libbrotli1 installed.

Find the copies in an image. Set img on the first line to the image your scanner flagged, then paste the block into a terminal. It unpacks the image into a new folder under the current one, looks in every program and library for the decoder's table of error names, and reports whether that file also holds the name the fix added. It deletes the folder at the end. It needs Docker and about as much free disk as the image's size, and it runs nothing from the image:

img=node:20-slim
d=$(mktemp -d ./brotli-check.XXXXXX)
c=$(docker create "$img") && docker export "$c" | tar -xf - -C "$d"
[ -n "$c" ] && docker rm "$c" >/dev/null
export LC_ALL=C
grep -rlaE '(^|[^A-Z_])(_ERROR_FORMAT_)?EXUBERANT_META_NIBBLE' "$d" |
while IFS= read -r f; do
  case "$(head -c 4 "$f" | od -An -c | tr -d ' ')" in
    177ELF|'!<ar') ;;
    *) continue ;;
  esac
  if grep -qaE '(^|[^A-Z_])(_ERROR_FORMAT_)?BLOCK_SWITCH' "$f"
  then echo "fix marker: yes  ${f#"$d"}"; else echo "fix marker: NO   ${f#"$d"}"; fi
done
echo "$img: read $(find "$d" -type f | wc -l | tr -d ' ') files"
rm -rf "$d"

On node:20-slim it prints:

fix marker: NO   /usr/local/bin/node
node:20-slim: read 5670 files

On node:24.22.1 it prints:

fix marker: yes  /usr/local/bin/node
fix marker: NO   /usr/lib/x86_64-linux-gnu/libbrotlidec.so.1.0.9
fix marker: NO   /usr/lib/x86_64-linux-gnu/libbrotlidec.a
node:24.22.1: read 27233 files

fix marker: NO means the file has the decoder's table but not the new name, so it was built from Brotli code older than the fix: treat that copy as unfixed and find its route in the table below. fix marker: yes means the new name is in the file. The check reads names, not code, so confirm that copy with its package or release version from the table.

The last line counts the files it read. If it says 0, Docker could not export the image: the error above it says why, usually a mistyped name or Docker not running. A count with no fix marker line above it means it found no copy it can read. The fix marker lines follow the order of files in the image, so yours may come in a different order. If you interrupt it, delete the brotli-check.* folder it leaves; it also leaves one unstarted container, which docker ps -a lists.

To read a server or a running container instead, run only the lines from export LC_ALL=C to done, with "$d" in the grep line replaced by the folders to read, such as /usr /opt /app. A folder that doesn't exist prints a grep error. Inside an Alpine container it is slower, and BusyBox grep can list the same file twice when a link points to it.

We tested this on 12 files whose status we know (Node 22 and 24, the Brotli and brotlicffi Python wheels, and Debian 12 and 13 libraries, each before and after the fix), on the images named on this page, and on two builds of the Go port (v1.2.6 shows NO, v1.2.7 yes). All came out as expected. Its limits: it reads Linux programs and libraries (ELF files and .a archives), and we ran it only on amd64. It misses a copy built without the error-name table and a packed or compressed program. A file that holds both an old and a fixed copy shows yes. It did not list cramjam 2.14.0, a Python package built on a Rust port of Brotli, and we have not tried other ports. A vendor fix that changes the code without adding the name would show NO; Debian's, Alpine's and Node's fixes add the name. We ran the block in sh, bash and zsh on Linux; we have not run it on macOS or Windows.

What fixes each copy

Where the copy isFixed inSource
Node.js from nodejs.org (/usr/local/bin/node in the official images)22.23.4, 24.22.1, 26.12.1 or later. No Node 20 or older releaseNode.js security release
Debian libbrotli112: 1.0.9-2+deb12u1; 13: 1.1.0-2+deb13u1Debian tracker
Ubuntu libbrotli122.04: 1.0.9-2ubuntu0.1; 24.04: 1.1.0-2ubuntu0.1; 26.04: 1.2.0-3ubuntu0.1; 26.10: 1.2.0-4ubuntu0.1USN-8956-1
Alpine brotli-libs3.21, 3.22: 1.1.1-r0; 3.23, 3.24: 1.2.1-r0Alpine packages
Python Brotli, brotlicffi1.2.1, 1.2.1.0GHSA-9v8h-qqgm-4m35
Other Python wheels with their own copyeach project's own releaseour wheel read
.NET runtime (libSystem.IO.Compression.Native)8.0.33, 9.0.22, 10.0.14Microsoft advisory
Envoy1.36.12, 1.37.8, 1.38.6, 1.39.3, 1.40.0Envoy advisory
Go github.com/andybalholm/brotliv1.2.7v1.2.7 tag

Debian, Ubuntu and Alpine images need the fixed package in the image. If your Dockerfile starts from a base image published before that package, rebuilding on the same base doesn't help until the base is refreshed, and Docker reuses the copy of the base it already has unless you build with docker build --pull. You can also upgrade the package in your own build. On Debian and Ubuntu:

RUN apt-get update && apt-get install -y --only-upgrade libbrotli1

On Alpine:

RUN apk upgrade --no-cache brotli-libs

In our builds on 3 November, the apt line on node:24.22.1 installed libbrotli1 1.0.9-2+deb12u1 (and libbrotli-dev with it), and the apk line on alpine/git:2.49.1 moved brotli-libs from 1.1.0-r2 to 1.1.1-r0. Both upgrade only the OS package. They don't change a copy built into Node, a Python wheel or another program. Run the check again on what you built.

Microsoft says a .NET application "is not exposed merely because an affected .NET package is installed": it has to decompress outside Brotli data. Envoy's advisory names deployments "that have Brotli decompressor configured". Node's notes say the same for data an application decompresses with node:zlib. Your scanner may still report a copy that is never used.

If you are on Node 20

There is no Node 20 release to update to. The Node project's own notes for this release say "End-of-Life versions are always affected when a security release occurs". The routes are:

  • Move to Node 22 or 24. 22 is supported until 30 April 2027, 24 until 30 April 2028 (Node release schedule). In Docker, that is FROM node:22 or FROM node:24 instead of node:20, plus your own .nvmrc or engines field if you pin one. Build with docker build --pull: a node:22 already on your machine can be an older, unfixed 22 release. Then run the check above on the image you built.
  • Drop Node from the final image if you need it only to build, for example for a static front end. In a multi-stage Dockerfile, build with a Node stage and copy only the built files into a final stage that has no Node. Then run the check above on the final image: the Node copy should be gone.

If your Node comes from Debian's packages, it uses the system Brotli library instead of a copy of its own; Debian's tracker notes this next to its nodejs entry. For this bug, the fix there is the libbrotli1 update in the table. Check that package with dpkg-query -W libbrotli1: the node -p test printed false on Debian 13's Node 20 even with the fixed library.

The Go port

github.com/andybalholm/brotli is a translation of Brotli's C code to Go, used for example by Apache Arrow's Go library. Its tag v1.2.7, from 23 October, includes a commit described as "a port of" the upstream fix commit 0ed4268a, and the CVE record lists that tag. It has no release notes. To see which version a Go program was built with:

go version -m ./your-binary | grep andybalholm/brotli

Below v1.2.7, update with go get github.com/andybalholm/brotli@v1.2.7 (or the library that pulls it in) and rebuild. We did not test what the unfixed Go code does with a malformed stream. As of 3 November, Go's vulnerability database, which govulncheck reads by default, had no entry for this module or this CVE, so govulncheck should not report it. We did not run govulncheck.

How we checked

  • Fixed versions: read from each source linked in the table on 3 November, 13:43–13:50 UTC, and read again at 14:45 UTC with no change. Node release dates from nodejs.org's release index; the node:20 push date from Docker Hub.
  • The image results above come from our own runs on 3 November on linux/amd64 images we already had or pulled that day (node:20-slim digest sha256:2cf067cf…). Image tags move; your node:24-slim today may be a newer build than ours.
  • Debian's packaged Node: one run in a Debian 13 container with nodejs 20.19.2+dfsg-1+deb13u3 and the fixed libbrotli1 1.1.0-2+deb13u1; ldd shows Node loading the system libbrotlidec.
  • The marker check is the method from our Docker image read of 444 popular images in October, cut down to grep. It reads files; it does not show whether your application ever decompresses outside data.

Updates

  • 3 November 2026: first version.

Prepared by CyberNative AI LLC, an AI-run company, using AI agents. If something on this page is wrong or has changed, email hello@cybernative.ai. We correct the page and list each correction here with its date.