On February 27, 2026, a developer quietly merged commit 0ed4268a into Google’s official GitHub repository for Brotli—the open-source compression library that powers a massive slice of modern Internet traffic. The commit looked like routine housekeeping: it added a single new error code (BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH) inside the C decompression engine (c/dec/decode.c).
No new release was cut. No security advisory was published. No CVE number was assigned.
For nearly eight months, that single commit sat in plain sight on a public repository while almost every major software ecosystem on Earth—Node.js, Python, Microsoft .NET, PHP, Bun, Envoy Proxy, Istio, ClickHouse, and Apache Arrow—continued bundling and shipping the unpatched, vulnerable C code to millions of production servers.
Then, on October 21, 2026, the bomb finally went off. Identified by researchers using OpenAI Codex Security (Khai Tran and Sylvie Mayer) and disclosed alongside emergency patches for Node.js, the flaw was officially assigned CVE-2026-111497 with a critical CVSS v4.0 score of 9.2. Within hours, security teams gave it a series of names that reflected its shape-shifting nature: first Node2Shell, then HTTP2Shell, Broccoli2Shell, and finally Brotli2Shell.
If PHP2Shell (which broke out the exact same week) proved that a single language runtime could collapse from within, Brotli2Shell proved something even more terrifying: modern software is built on a hidden monoculture of "vendored" C libraries. And when one of those foundational bricks cracks, it doesn't matter whether you write JavaScript, Python, C#, or PHP—everybody bleeds at once.
Here is the inside story of how a misunderstood false return value inside a data decompressor turned an 8-byte memory buffer into a universal Remote Code Execution (RCE) weapon.
1. The Invisible Monoculture: What Is Brotli and Why Is It Everywhere?
Every time you load a webpage, call a REST API, or trigger a webhook, the data traveling across the wire is compressed so it arrives faster. For decades, gzip was the king of web compression. Then in 2015, Google introduced Brotli (Content-Encoding: br), a compression algorithm capable of shrinking files 15% to 20% more efficiently than gzip.
Today, Brotli is part of the plumbing of the Internet. Because writing a high-speed decompressor from scratch is brutally complex, virtually nobody writes their own. Instead, language runtimes take Google’s reference C decoder library (libbrotli versions 1.0.3 through 1.2.0) and bake a copy directly into their own source code—a practice known as vendoring.
Think of it like an airbag manufacturer:
- Node.js, Microsoft .NET, Python, Bun, and PHP are completely different car brands with completely different engines.
- Yet, deep inside the steering wheel, they all installed the exact same model of airbag from the same factory.
When an application receives compressed data with the br header, it hands those raw bytes straight to Google’s C decoder. And inside that decoder, a fatal logic trap had been waiting for years.
2. The Mechanism: One Return Value, Two Meanings, and an 8-Byte Overflow
At its core, Brotli2Shell (CVE-2026-111497) is the deadly marriage of two classic software weaknesses: an Unchecked Return Value (CWE-252) that triggers an Out-of-Bounds Write (CWE-787).
To understand how it works without getting lost in C pointers, imagine how a Brotli compressed stream is structured:
- A Brotli stream divides data into categories (literals, insert-and-copy commands, and distances), and splits each category into blocks.
- Right at the beginning of the stream, the header declares a strict rule: "For this category, I will only ever use 1 single block type."
- Later in the stream, a maliciously crafted file breaks its own promise and sends a command saying: "Switch to a different block type now!"
Since the stream promised there was only one block type, asking to switch to another is illegal—the file is corrupt and must be rejected immediately.
The Fatal Miscommunication
Inside Google’s Brotli C code (c/dec/decode.c), the function responsible for reading this instruction is called DecodeBlockTypeAndLength().
In the vulnerable versions (1.0.3 to 1.2.0), when DecodeBlockTypeAndLength() encountered that illegal block switch, it returned a simple boolean: false.
Here is the catch: in that exact same function, false was already used to mean something completely innocent: "I ran out of network bytes mid-read; please pause, wait for the network to send the next packet, and feed me more data."
Think of a shipping clerk loading boxes into a tiny 8-slot locker (an 8-byte internal buffer inside the decoder's state machine):
- The clerk spots a bomb in a package (the illegal block switch) and yells "STOP!" (
false). - The conveyor belt operator (the streaming caller) hears "STOP!", but assumes the clerk is actually saying: "I don't have enough boxes to finish the row, send me more!" (
BROTLI_DECODER_NEEDS_MORE_INPUT). - Every time a new network chunk arrives, the operator forces more boxes into that tiny 8-byte internal buffer without resetting the counter.
- By the 9th byte, the boxes smash through the back wall of the locker, overwriting adjacent server memory (Out-of-Bounds Write) with whatever raw bytes the attacker sent over the network.
With commit 0ed4268a, Google finally separated the two meanings by introducing a dedicated fatal error code (-17, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH), forcing the decoder to abort immediately instead of begging for more bytes.
3. A Two-Way Street: Attacking Servers AND Attacking Clients
Unlike traditional server-side bugs, Brotli2Shell works in both directions across the HTTP pipeline. Any process that decodes untrusted Brotli data is vulnerable:
Direction A: The Inbound Server Attack (Content-Encoding: br)
An attacker sends a POST, PUT, or PATCH request to an API endpoint with the header Content-Encoding: br and a maliciously compressed body. If the backend framework automatically decompresses incoming request bodies (such as Express 5 / body-parser v2, Fastify, Koa, or ASP.NET Core with request decompression enabled), the server feeds the payload straight into the vulnerable C decoder before your route handler even sees it.
Direction B: The Outbound "Boomerang" Attack (Accept-Encoding: br)
What if your server doesn't accept compressed POST requests? You can still get owned in reverse.
Modern web applications constantly fetch URLs provided by users: webhook testers, link preview generators (Slack/Discord style), RSS aggregators, AI agent web scrapers, or WordPress XML-RPC pingbacks.
When your backend uses Node's built-in fetch(), Python's requests / httpx / aiohttp (when brotli or brotlicffi is installed), or .NET's HttpClient to visit an attacker's URL, the attacker's server replies with a poisoned HTTP response labeled Content-Encoding: br. Even if your client didn't ask for Brotli in its Accept-Encoding header, many HTTP clients will dutifully decompress a br response automatically—triggering the memory overwrite inside your server.
(In fact, on November 2, 2026, researchers at Wordfence demonstrated full unauthenticated RCE against a default WordPress server on Debian simply by abusing WordPress's unauthenticated XML-RPC pingback feature to make PHP/cURL fetch a malicious Brotli response from an attacker-controlled server!)
4. The Timeline: From Silent Commit to Global Scramble
The anatomy of the disclosure reveals how dangerous the gap between an upstream commit and downstream releases can be:
- February 27, 2026: Google commits fix
0ed4268atogoogle/brotlion GitHub. No release is tagged; no CVE is filed. The fix sits in public view for nearly 240 days. - October 6 – 15, 2026: Envoy Proxy quietly ships patched releases on October 6, followed by Istio on October 15, which first mentions the CVE identifier
CVE-2026-111497in a security bulletin. - October 21, 2026 (10:38 UTC): Node.js publishes emergency security releases (
v22.23.4,v24.22.1,v26.12.1), crediting OpenAI Codex Security for reporting the vulnerability and Matteo Collina and Antoine du Hamel for the Node.js fix. - October 21, 2026 (15:47 UTC): Barely five hours after the Node.js patches drop, the first public Proof-of-Concept (PoC) repository appears on GitHub. That evening (~19:27 UTC), Google officially releases Brotli
1.0.10,1.1.1, and1.2.1. - October 22, 2026: Imperva demonstrates reliable Remote Code Execution on an unmodified, official
Node.js 24.22.0Linuxarm64image. On the same day, patched Python packages (Brotli 1.2.1andbrotlicffi 1.2.1.0) hit PyPI. - October 24 – 25, 2026: Public RCE exploits for
x86_64Node.js 24.22.0 with Express 5 surface. ELLIO and OtterSec reproduce RCE across dozens of configurations. On Sunday, October 25, Microsoft takes the rare step of pushing an out-of-band (OOB) emergency security update for .NET 8, 9, and 10, two weeks ahead of Patch Tuesday. - October 26 – 27, 2026: PHP releases emergency
-1rebuilds for Windows (8.5.12-1,8.4.27-1,8.3.36-1,8.2.35-1). Meanwhile, ELLIO honeypots capture thenode2shell-checkscanner and a 23 MB in-the-wild exploit payload targeting Node.js 24.22.0. On October 27, CISA adds CVE-2026-111497 to its Known Exploited Vulnerabilities (KEV) catalog, mandating federal patching by November 10.
5. The Patch Matrix & The "Vendored vs. System Library" Nightmare
Why is remediating Brotli2Shell driving security engineers crazy? Because you don't have one copy of Brotli on your server—you probably have five.
- Your Linux OS has a system library (
/usr/lib/x86_64-linux-gnu/libbrotli1) used bycurl,apt, and Debian's native packages. - Your official Node.js binary or Docker container ignores the OS library and compiles its own vendored copy of Brotli directly into the
nodeexecutable. - Your Python virtual environment has its own compiled C wheel inside
site-packages/brotli. - Your self-contained .NET microservice or Bun runtime carries its own static copy.
Updating apt-get upgrade libbrotli1 on Ubuntu or Debian will not fix an official Node.js container, a Python venv, or a self-contained .NET binary. You have to hunt down and patch every single ecosystem independently:
| Ecosystem / Component | Vulnerable Versions | Patched Versions | End-of-Life (EOL) Trap |
|---|---|---|---|
Google libbrotli (Upstream) |
1.0.3 – 1.2.0 |
1.0.10, 1.1.1, 1.2.1 |
— |
| Node.js | < 22.23.4, < 24.22.1, < 26.12.1 |
22.23.4, 24.22.1, 26.12.1 |
Node 20.x, 23.x, 25.x receive NO fix |
| Bun Runtime | <= 1.4.3 |
1.4.4+ |
— |
| Python (PyPI) | Brotli < 1.2.1, brotlicffi < 1.2.1.0 |
Brotli 1.2.1, brotlicffi 1.2.1.0 |
Transitive dependencies in frozen requirements.txt |
| Microsoft .NET | .NET 6, 7, 8, 9, 10 (pre-Oct 25) |
.NET 8.0.33, 9.0.22, 10.0.14 |
.NET 6 & 7 receive NO fix (.NET 8 & 9 hit EOL Nov 10, 2026!) |
| PHP (Windows Builds) | 8.2.x – 8.5.x (pre-Oct 26) |
8.2.35-1, 8.3.36-1, 8.4.27-1, 8.5.12-1 |
PHP 8.1 and older |
(Note: Standard Nginx and Apache mod_brotli are not vulnerable in default configurations because they only compress outbound responses and do not decompress incoming Content-Encoding: br requests—unless specific third-party Lua/decompression modules or F5 NGINX WAF profiles are active).
Technical Appendix: Detection Engineering, WAF Rules, and Threat Hunting for Brotli2Shell
Because CVE-2026-111497 operates inside native C memory (libbrotli), an exploit either succeeds silently inside the worker process or triggers a fatal native crash (SIGSEGV / SIGABRT) when the heap layout doesn't match the attacker's offset. Even a failed RCE attempt gives attackers two dangerous primitives: Denial of Service (DoS) via rapid worker crashes, and Process Memory Disclosure (leaking adjacent heap data, API keys, or session tokens into responses or core dumps).
1. Perimeter & Application Mitigations (Stopgap Controls)
Until every container and runtime in your fleet is rebuilt, cut off the attack vectors at the reverse proxy, load balancer, and application middleware:
A. Nginx Reverse Proxy (Strip & Reject Inbound Brotli)
Very few legitimate web browsers or API clients send requests compressed with Brotli (browsers almost exclusively use br to receive responses). Rejecting inbound Content-Encoding: br at the edge immediately neutralizes the server-side push vector:
# Map any Content-Encoding header containing the token "br" (e.g., "br" or "gzip, br")
map $http_content_encoding$reject_inbound_brotli {
default 0;
"~*(^|,|\s)br($|,|\s)" 1;
}
server {
listen 443 ssl;
server_name api.example.com;
# Block inbound Brotli-compressed requests with HTTP 415 (Unsupported Media Type)
if ($reject_inbound_brotli) {
return 415;
}
location / {
# Optional: Strip "br" from Accept-Encoding if upstream is vulnerable to outbound bugs
proxy_pass http://nodejs_backend;
}
}
B. ModSecurity / OWASP CRS Rule (Inbound Content-Encoding: br Block)
# Rule 1114970: Block inbound requests compressed with Brotli (CVE-2026-111497)
SecRule REQUEST_HEADERS:Content-Encoding "@rx (?i)\bbr\b" \
"id:1114970,\
phase:1,\
deny,\
status:415,\
log,\
msg:'Brotli2Shell CVE-2026-111497 - Inbound Brotli Content-Encoding Blocked',\
tag:'cve-2026-111497',\
severity:'CRITICAL'"
C. F5 BIG-IP iRule Mitigation
As recommended in F5 Advisory K000163902, drop requests carrying the br encoding token at the load balancer:
when HTTP_REQUEST {
foreach encoding [HTTP::header values "Content-Encoding"] {
if { [regexp -nocase {(^|,)[ \t]*br[ \t]*(,|$)}$encoding] } {
log local0. "CVE-2026-111497: Rejected Brotli request from [IP::client_addr]:[TCP::client_port]"
reject
return
}
}
}
D. Code-Level Hardening (Node.js Express & fetch())
If you cannot touch the load balancer, disable automatic body inflation in Express and strip br from outbound fetch() calls:
// 1. Inbound Mitigation: Disable automatic decompression in Express / body-parser
app.use(express.json({ inflate: false }));
app.use(express.urlencoded({ extended: true, inflate: false }));
// 2. Outbound Note: Removing 'br' from Accept-Encoding helps with polite servers,
// but a malicious server can STILL force 'Content-Encoding: br' in its reply!
// Never pass untrusted user URLs to native fetch() on unpatched Node.js.
2. YARA Signatures (Network Payloads & Recon Scanners)
Use these YARA rules to inspect HTTP traffic captures, WAF request dumps, or proxy buffers for the node2shell-check scanner and oversized Brotli heap-spray payloads (such as the ~23 MB payloads captured by ELLIO honeypots).
rule EXP_Brotli2Shell_CVE_2026_111497_Scanner_And_Exploit {
meta:
description = "Detects Brotli2Shell / Node2Shell (CVE-2026-111497) scanning tools and malformed Brotli POST requests"
author = "Security Engineering Team"
date = "2026-11-03"
cve = "CVE-2026-111497"
severity = "Critical"
strings:
// HTTP Request Methods & Headers
$m_post = "POST " ascii startswith
$m_put = "PUT " ascii startswith
$ce_br1 = "Content-Encoding: br" ascii nocase
$ce_br2 = "Content-Encoding: gzip, br" ascii nocase
$ce_br3 = "Content-Encoding: deflate, br" ascii nocase
// Known reconnaissance scanner User-Agents & markers observed by ELLIO
$ua_scanner1 = "node2shell-check" ascii nocase
$ua_scanner2 = "brotli2shell" ascii nocase
$ua_scanner3 = "http2shell" ascii nocase
// WordPress XML-RPC Pingback vector (Outbound Boomerang trigger)
$xmlrpc_ping = "<methodName>pingback.ping</methodName>" ascii
condition:
(1 of ($ua_scanner*)) or
((1 of ($m_*)) and (1 of ($ce_br*)) and filesize > 5MB) or
($xmlrpc_ping and (1 of ($ce_br*)))
}
3. Fleet Auditing & Threat Hunting Queries
Because software composition analysis (SCA) scanners often miss C libraries compiled directly into language binaries, run these commands across your hosts and containers to locate vulnerable decoders:
Bash Fleet Audit Script (Check OS, Node.js, Python, and .NET Copies)
#!/usr/bin/env bash
echo "=== [1] Checking System libbrotli1 (Debian/Ubuntu/RHEL) ==="
dpkg -l | grep -i brotli 2>/dev/null || rpm -qa | grep -i brotli 2>/dev/null
echo "=== [2] Checking Running & Installed Node.js Binaries ==="
# Vulnerable if < 22.23.4, < 24.22.1, < 26.12.1, or any 20.x / 23.x / 25.x
for node_bin in $(which -a node 2>/dev/null)$(find / -name "node" -type f -executable 2>/dev/null | grep -E "bin/node$"); do
echo -n "$node_bin -> "
$node_bin -e "console.log('Node:', process.version, '| Bundled Brotli:', process.versions.brotli)" 2>/dev/null
done
echo "=== [3] Checking Python Virtualenvs for Brotli & brotlicffi ==="
# Vulnerable if Brotli < 1.2.1 or brotlicffi < 1.2.1.0
find / -name "pip" -type f -executable 2>/dev/null | while read -r pip_bin; do
$pip_bin list 2>/dev/null | grep -E -i "^(brotli|brotlicffi)\s+" && echo " ^ Found in: $pip_bin"
done
echo "=== [4] Checking .NET Shared Runtimes ==="
# Vulnerable if < 8.0.33, < 9.0.22, < 10.0.14, or any .NET 6.x / 7.x
dotnet --list-runtimes 2>/dev/null
Sigma / EDR Runtime Rule (Node.js / Python / .NET Post-Exploitation)
Adapted from Sysdig TRT and runtime detection telemetry, this rule detects when a node, python, dotnet, or bun process redirects a network socket to standard streams (stdin/stdout) or spawns a system shell following a Brotli heap overwrite:
title: Brotli2Shell / Node2Shell Post-Exploitation Shell Spawn
id: 3a9c1149-7b2e-4f10-8a11-202611149700
status: experimental
description: Detects shell utilities or reverse shell socket redirections spawned by runtimes embedding the vulnerable Google Brotli decoder (CVE-2026-111497).
author: Security Engineering
date: 2026/11/03
logsource:
category: process_creation
product: linux
detection:
selection_parent:
ParentImage|endswith:
- '/node'
- '/nodejs'
- '/bun'
- '/python'
- '/python3'
- '/dotnet'
- '/envoy'
selection_child:
Image|endswith:
- '/sh'
- '/bash'
- '/dash'
- '/zsh'
- '/curl'
- '/wget'
- '/nc'
- '/ncat'
- '/socat'
condition: selection_parent and selection_child
falsepositives:
- Legitimate CI/CD runners or build scripts invoking child shells from Node.js or Python
level: critical
tags:
- attack.initial_access
- attack.execution
- attack.t1190
- attack.t1059
- cve.2026.111497