On Wednesday, while I was busy with the PHP2Shell patches, Node.js published another critical advisory: CVE-2026-111497, dubbed node2shell. It looked like a Node problem, so I left it for later. That was a mistake, because the bug isn’t in Node but in Brotli, Google’s compression library, and over the week it has turned up in Python, C++, Bun, Envoy, Istio, curl… Aikido sums it up in an article that now calls it HTTP2Shell, because almost anything that speaks HTTP with compression is at risk.
And when I checked, it turned out that the Docker image I updated on Wednesday for PHP2Shell was vulnerable too.
What Brotli is and what went wrong
Brotli is a lossless compression format widely used in HTTP to compress HTML, CSS and JavaScript: it’s the br you see in the Accept-Encoding and Content-Encoding headers. It’s implemented by a C library, and almost everyone uses that same library, packaged one way or another.
The bug is an out-of-bounds write in the decoder, in versions 1.0.3 to 1.2.0, with a CVSS v4 score of 9.2. It lives in DecodeBlockTypeAndLength(), the function that handles block switches inside the compressed stream. In the vulnerable versions it returns a boolean, and false can mean two very different things: “this data is malformed” or “I need more data”. Callers can’t tell them apart, and on the fast path they don’t even look at the result. A crafted stream can force an impossible block switch, and the decoder carries on with a state that is no longer valid until it writes past the buffer.
What makes it serious is that it works in both directions:
- A malicious client can attack a server that decompresses Brotli request bodies.
- A malicious server can attack a client that accepts compressed responses.
Imperva has published a proof of concept that runs code on the official Node.js 24.22.0 image, both with the built-in fetch() receiving a response and with Express 5 and body-parser 2 receiving a request (their raw() and json() parsers decompress Brotli). On 24.22.1, the same inputs are rejected. A plain Node HTTP server doesn’t decompress the bodies it receives, so the risk depends on which middleware you use.
The odd part: the fix had been in the code for months
OpenAI found the bug in February and reported it as remote code execution. The root cause was fixed in the Brotli repository back then (commit 0ed4268), but it wasn’t treated as a security issue and no release shipped with the fix until now. According to those who have diffed the code, Brotli 1.2.1 only changes the version number and that commit.
It’s a recurring pattern: fixing a bug doesn’t help much if the fix never reaches a release, and when the library is bundled inside other projects, each one has to ship its own update. That’s why the list of affected software has grown day by day.
Fixed versions
| Project | Fixed versions |
|---|---|
| Brotli (upstream) | 1.2.1, plus 1.0.10 and 1.1.1 for older branches |
| Node.js | 22.23.4, 24.22.1, 26.12.1 |
Python (Brotli, brotlicffi) | 1.2.1 and 1.2.1.0 |
| Bun | 1.4.4 |
| Envoy | 1.40.0, 1.39.3, 1.38.6, 1.37.8 |
| Istio | 1.29.9, 1.30.6, 1.31.2 |
Apache with mod_brotli and Nginx don’t decompress Brotli by default (they use it to compress), so the web server itself usually isn’t the problem. What sits behind it can be.
There’s an important detail for Python: requests, httpx and aiohttp load Brotli or brotlicffi automatically if they’re installed, even if your code never uses them directly. And according to Aikido, as of the 23rd the advisory databases still didn’t map the CVE to those PyPI packages, so pip-audit, Dependabot and Safety won’t warn you yet. You have to check by hand:
pip list 2>/dev/null | grep -i brotli
My PHP image had it too
Four days ago I wrote about how I had updated drzippie/php-nginx, my image with PHP-FPM and Nginx on Alpine, to fix PHP2Shell. I thought Brotli had nothing to do with me, but the image includes Node.js to build assets, and on Alpine Node doesn’t bundle its own copy of Brotli: it uses the system one. So I checked:
docker run --rm --entrypoint sh drzippie/php-nginx:2026-10-21 -c \
'node -p process.versions.brotli; apk info -v | grep brotli; ldd $(which node) | grep brotli'
1.2.0
brotli-libs-1.2.0-r1
libbrotlidec.so.1 => /usr/lib/libbrotlidec.so.1
Brotli 1.2.0, vulnerable. And Node isn’t the only user. Looking for everything else that links libbrotlidec inside the image, the list is long:
libcurl, and with it PHP’scurlextension, which decompressesbrresponses.- Swoole, whose HTTP client also accepts compressed responses.
- PHP’s
brotliextension, withbrotli_uncompress(). - FreeType, which uses Brotli for WOFF2 fonts, and through it GD and ImageMagick.
Every one of those paths leads to the same decoder.
In my case the real risk is low, because Node is only used at build time and my PHP applications don’t decompress Brotli bodies sent by anyone. But a curl request to an external server that replies with Content-Encoding: br does go through that decoder, and I’d rather not rely on no external service ever being malicious.
The good news is that Alpine 3.24 already ships brotli-libs 1.2.1 and nodejs 24.22.1, so there’s nothing to compile. Rebuilding the image without cache is enough, so it doesn’t reuse layers with the old packages:
docker build --pull --no-cache -t drzippie/php-nginx .
And checking it:
v24.22.1
1.2.1
brotli-libs-1.2.1-r0
nodejs-24.22.1-r0
On top of that, Alpine has now packaged PHP 8.4.27, so the stage I added to compile PHP myself is no longer needed and the image is back on the official package.
| Tag | PHP | Brotli | Node.js | Notes |
|---|---|---|---|---|
latest, 2026-10-25 | 8.4.27 | 1.2.1 | 24.22.1 | Also fixes CVE-2026-111497 |
2026-10-21 | 8.4.27 | 1.2.0 | 24.18.1 | Fixes PHP2Shell, vulnerable to HTTP2Shell |
My takeaway here is that --no-cache matters. If you rebuild an image with cache, Docker may reuse the apk add layer and hand you exactly the same vulnerable library under a new tag.
If you can’t update yet
Aikido suggests a few measures to buy time:
- Reject requests with
Content-Encoding: brat the proxy, load balancer or middleware, before the body reaches the decoder. Watch out for upper case and for lists of encodings. - For outgoing requests, removing
brfromAccept-Encodingis not enough: a malicious server can send Brotli anyway, and libraries like urllib3 pick the decoder from the response header. You need to disable Brotli decompression or reject those responses before processing them. - In Python, if Brotli is optional, uninstall
Brotliandbrotlicffiand drop extras likehttpx[brotli]. The libraries keep working with gzip and deflate.
And then the usual: update dependency lock files, rebuild images, deploy and check that what’s actually running uses the fixed version, not just what the Dockerfile says.
What I take away
This week brought two critical bugs with very different mechanics, but a common lesson. With PHP2Shell, the problem was in the language I use every day and it was easy to know whether I was affected. With HTTP2Shell, it was in a compression library I never consciously chose, and it got into my image through half a dozen different paths. Knowing what’s inside your containers, beyond what you wrote in the Dockerfile, is what lets you answer “does this affect me?” in minutes.
Sources: Node2Shell evolves to HTTP2Shell by Aikido, Node2Shell: From Brotli Data to Remote Code Execution by Imperva and the Brotli fix commit.




