|

The Brotli flaw CVE-2026-111497: one library everywhere, in several copies

Brotli is a compression algorithm designed by Google, used everywhere on the web: most browsers accept it, most web servers and CDNs use it to compress pages. A flaw in its decoder, CVE-2026-111497, published on 21 October 2026, allows an out-of-bounds write to a memory buffer when decompressing data crafted for it. This post explains…

Brotli is a compression algorithm designed by Google, used everywhere on the web: most browsers accept it, most web servers and CDNs use it to compress pages. A flaw in its decoder, CVE-2026-111497, published on 21 October 2026, allows an out-of-bounds write to a memory buffer when decompressing data crafted for it. This post explains why a flaw in a compression library is harder to handle than a flaw in an application, how I found every copy of Brotli on my servers, and why, nine days later, one machine still wasn’t fixed despite automatic updates being active.

What the flaw is

A compression decoder reads a byte stream describing, very compactly, how to rebuild the original data: literal symbols, references to data already produced (“copy 300 bytes located 4,000 bytes back”), block sizes. The decoder must write the result into an output buffer, checking at every step that what it’s about to write fits in that buffer. An out-of-bounds write happens when one of those checks is missing or wrong in a particular case: a stream crafted to reach that case makes the decoder write past the end of the buffer, into neighbouring memory. Depending on what’s there, the result ranges from a crash to code execution.

The key point is who supplies the compressed data. The dangerous scenario is a program decompressing data coming from outside: an HTTP client receiving a compressed response from a malicious server, or a server accepting compressed request bodies sent by a client. Compressing your own data poses no problem.

The real problem: Brotli is everywhere, in several copies

A flaw in nginx is fixed by updating nginx. A flaw in a compression library hides in dozens of programs, in two different ways.

The system shared library

rpm -q --whatrequires 'libbrotlidec.so.1()(64bit)'
freetype-2.13.2-8.el10.x86_64        # fonts: PHP gd/imagick, image generation
exiv2-libs-0.28.3-5.el10.x86_64      # image metadata
libjxl-0.10.4-1.el10_1.x86_64        # images JPEG XL
libsoup3-3.6.5-5.el10.x86_64         # GNOME HTTP client
libcurl-8.12.1-4.el10_2.7.x86_64     # curl, hence PHP curl, git, dnf…
dotnet-runtime-9.0-9.0.21-1.el10_2   # .NET applications

The libbrotli package provides a shared library other programs load at startup. The rpm -q --whatrequires query lists those depending on it directly: curl (and therefore PHP’s curl extension, git, part of the system tools), the font and image libraries PHP uses to generate thumbnails, the .NET runtime. This case is the simplest: update libbrotli, and all these programs use the fixed version… at their next start.

Bundled copies

node -p "process.versions.brotli"                                    # 1.2.0 (fixed)
/opt/etherpad/nodejs/bin/node -p "process.versions.brotli"           # 1.2.0 (fixed)
/usr/share/wazuh-dashboard/node/bin/node -p "process.versions.brotli" # 1.0.9 (vulnerable, Node 18 end of life)

Many programs don’t use the system library but bundle their own copy, compiled into their executable. Node.js is the typical example: it contains its own Brotli, zlib, OpenSSL. And applications that bundle Node.js themselves (a collaborative editor installed in /opt, the Wazuh dashboard) have their copy of Node, hence their copy of Brotli. Updating libbrotli changes nothing for them. Only a new version of the software bundling them fixes them. On one of my servers, the Wazuh dashboard bundles Node 18, end of life, with Brotli 1.0.9: no fixed Node 18 will ever exist; you have to wait for Wazuh to update its embedded Node.

What I did, in order

  • inventory: packages depending on libbrotli, then every Node executable on each machine (find / -xdev -type f -name node) with its bundled Brotli version (node -p process.versions.brotli);
  • exposed bundled copies first: the collaborative editor, reachable from the Internet, moved the same day to a fixed Node (24.22.1); the system’s Node installations moved to the NodeSource repository, version 26, which bundles the fixed Brotli 1.2.0;
  • a stopgap on the web server, for cases where what the client sends gets decompressed;
  • the system library, as soon as the distribution’s fix was available;
  • restarts of everything that loaded the old library;
  • access to the Wazuh dashboard restricted to admin addresses only, pending a fixed release.

The stopgap

# snippets/server.d/reject-brotli-request.conf : refuse Brotli-compressed request BODIES
if ($http_content_encoding ~* "br") {
    return 415;
}

An HTTP client can announce, through the Content-Encoding: br header, that its request body is Brotli-compressed. No browser does that spontaneously, and none of my applications need it. Refusing those requests with a 415 code (“unsupported media type”) at the web server prevents a malicious compressed body from reaching an application that would decompress it. It’s a narrow measure, with no side effect for normal use, and easy to remove.

Read the changelog, not the version number

rpm -q --changelog libbrotli | head -4
* Thu Oct 22 2026 RHEL Packaging Agent <…> - 1.1.0-8.1
- CVE-2026-111497 brotli: Out-of-Bounds Write in Google Brotli Decoder
- Resolves: RHEL-290261

The Brotli project fixed the flaw in its 1.0.10, 1.1.1 and 1.2.1 releases. The distribution kept version 1.1.0 and backported the fix into a new release, 1.1.0-8.1. A scanner comparing “1.1.0” with the list of vulnerable versions will wrongly conclude the package is vulnerable. The only reliable check is the CVE’s presence in the installed package’s changelog.

Restart what uses the old copy

# processes still holding the OLD library in memory (file replaced on disk)
grep -l 'libbrotli.*(deleted)' /proc/[0-9]*/maps 2>/dev/null | cut -d/ -f3 \
  | xargs -r -I{} sh -c 'echo "$(cat /proc/{}/comm) {}"' | sort -u
needs-restarting -s          # the same, service by service

When a package replaces a library, the file on disk changes, but every already-running process keeps the old version in memory, whose file no longer exists. The kernel flags it in /proc/<pid>/maps with (deleted). The needs-restarting command does that check for you and lists the services to restart. A media server, a PHP server, a .NET service can run for weeks without restarting: without this check, they stay vulnerable long after the update.

The trap: a fix without a security advisory

MachineSystem libraryFixed?Why
main web server1.1.0-8.el10_2.1yesmanual update on 22–23 October
lab1.1.0-8.el10_2.1yessame
mail server1.1.0-8.el10_2.1yessame
Wazuh server1.1.0-8.el10_2no (as of 30 October)automatic updates in “security” mode only, and no advisory published
Raspberry Pi (directory)1.0.9-10.el9_8noautomatic updates disabled (not enough memory for dnf)

Nine days after publication, two machines weren’t fixed. For the Raspberry Pi, it’s a deliberate choice: automatic updates are off there because dnf runs out of memory, and they must be done by hand. The Wazuh server is more instructive. Automatic updates are active there, configured to install security updates only (upgrade_type = security in dnf-automatic). But the distribution published the fixed package without an associated security advisory: to dnf, it’s an ordinary update. Security-only mode never installed it, and my daily CVE monitoring script, which relied on those same advisories, didn’t see it either.

The same day, a new kernel fixing sixteen CVEs appeared, also without an advisory. I changed my script so that, for each pending update, it compares the CVEs named in its changelog with those of the installed package: it now detects these “silent” fixes. And the question of the automatic update mode (security only, or all updates) comes up again: “security” mode is only as good as the published advisories.

In short

  • a flaw in a library hides in every program that loads it, and in every one that bundles a copy;
  • inventory in two steps: dependencies of the system package, then bundled copies (Node.js, Python, Java, Go…);
  • a library update only takes effect when processes restart: needs-restarting;
  • the changelog is authoritative, not the version number;
  • “security” automatic updates only see what’s published as such. Check pending updates that name CVEs.

This post is part of a series of sysadmin tutorials (tag “how-to”).
Series “How-to”, tutorial 2/57. · ← Previous tutorial · Next tutorial → (coming 31 Oct) · Version française

Similar Posts

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *