On 21 October 2026 Node.js published a security release with ten fixes. One of them, CVE-2026-111497, is an out-of-bounds write in Google’s Brotli decoder, which Node bundles into its own binary. Node.js says the flaw permits remote code execution.
The two Brotli packages on PyPI shipped patched builds on 22 October, and by the next day the Python side had its own name. Node2Shell, Python2Shell and the later HTTP2Shell all point to the same CVE.
The two exposures
| Node.js | Python | |
|---|---|---|
| CVE | CVE-2026-111497 | CVE-2026-111497 |
| Vulnerable artifact | Brotli decoder bundled in 22.x, 24.x and 26.x before the fix, and in every end-of-life line | PyPI Brotli below 1.2.1, brotlicffi below 1.2.1.0 |
| Fixed in | 22.23.4, 24.22.1, 26.12.1 | Brotli 1.2.1, brotlicffi 1.2.1.0 |
| Severity | High in the Node.js advisory; CVSS v4.0 9.2 (critical) in Google’s CVE record | Same CVE record, tracked for PyPI as PYSEC-2026-4322 and PYSEC-2026-4323 |
| Exploited in the wild | None on record as of 23 October | None on record as of 23 October |
Inside the flaw
A Brotli stream is built from blocks and can switch between block types as it goes. The decoder in older versions treated an invalid block switch as “the input ended early, send more bytes” instead of as a format error. It kept the stream open and appended new data to an internal buffer that holds eight bytes.
Enough malformed input overflows that buffer and corrupts adjacent memory. The outcome then depends on heap layout: a crash, a leak of process memory, or control over the instruction pointer that turns into code execution. The CVE record lists two weaknesses, an unchecked return value (CWE-252) and an out-of-bounds write (CWE-787). Node.js credits OpenAI Codex Security, Khai Tran and Sylvie Mayer with the report.
Google committed the fix to the Brotli repository on 27 February 2026. The commit message called it earlier detection of invalid streams and made no mention of security. The patched releases 1.0.10, 1.1.1 and 1.2.1 appeared on 21 October, nearly eight months later. Google’s CVE record scores the flaw 9.2 under CVSS v4.0; Node.js rates its own advisory high. The affected range starts at 1.0.3, which dates to early 2018, so the flaw shipped for more than eight years.
Node2Shell: three ways in
Brotli data reaches a Node process through request bodies, through responses it fetches, and through direct calls to the library.
Request bodies are the obvious way in. Express 5 ships body-parser 2, and its parsers (express.json(), express.raw() and the others) decompress bodies sent with Content-Encoding: br by default. Any client can hand the decoder arbitrary bytes without an account. Express 4 uses body-parser 1.x, which rejects Brotli bodies with a 415, and a bare node:http server does not decode request bodies on its own.
Responses get less attention. fetch() decompresses Brotli automatically and picks the decoder from the response header, whatever your request advertised. A reply from a server the attacker controls reaches the decoder before any parsing code of yours runs. Webhook deliveries, URL previews, image proxies and every “fetch this link for me” feature carry the exposure.
The third path is a direct call to node:zlib, in your code or in a dependency. A grep finds those in a minute.
Imperva’s red team reproduced code execution in fresh containers running the official Node.js 24.22.0 image on Linux arm64, through a response consumed by fetch() and through request bodies parsed by Express 5 with body-parser 2. The exploit took several attempts, 24.22.1 rejected the same input, and Imperva has not released the code.
Public repositories appeared within hours of the release. The published proofs of concept crash unpatched Node.js; no public code has achieved remote execution so far. CISA’s assessment in the CVE record listed exploitation as none on 23 October, and the CVE is not in CISA’s Known Exploited Vulnerabilities catalog.
The fixed releases are 22.23.4, 24.22.1 and 26.12.1. Bun bundles the same decoder and shipped its fix in 1.4.4. Node.js 25.x, 23.x, 20.x and older are end of life and will not receive one.
An image pinned to node:24.22.0 stays vulnerable however often you rebuild. Move the pin first.
Python2Shell: the same flaw, a day later
No CPython interpreter is vulnerable here. Google’s Brotli binding and the CFFI alternative brotlicffi compile the same C decoder into an extension module. Both projects uploaded patched builds to PyPI on 22 October: Brotli 1.2.1 and brotlicffi 1.2.1.0. The brotlicffi changelog names the CVE, and so do Google’s release notes for 1.2.1. The Brotli project page on PyPI does not, so a glance at PyPI tells you nothing.
Exposure follows the HTTP clients. When Brotli or brotlicffi is installed, urllib3 advertises Brotli support and decodes what comes back; requests inherits that through urllib3. httpx with the brotli extra, aiohttp and Scrapy behave the same way. Your code never has to import the package. urllib3 also decodes a Brotli response when the request sent Accept-Encoding: identity, because it reads the response header. A Python service that fetches pages, images or webhooks from servers you do not control passes attacker data to the decoder.
For about a day neither package appeared in the advisory databases that scanners read, so a clean report proved nothing. On 23 October GitHub added both packages to GHSA-9v8h-qqgm-4m35, and PYSEC-2026-4322 (Brotli) and PYSEC-2026-4323 (brotlicffi) followed. pip-audit now flags vulnerable versions.
The upgrade is pip install -U "Brotli>=1.2.1" "brotlicffi>=1.2.1.0", followed by a rebuild of every image or wheel that bundles them. If you use Poetry, uv or pip-tools, raise the constraint and regenerate the lockfile.
Where shipping today is impossible, uninstalling both packages removes the vulnerable decoder. The HTTP clients keep gzip and deflate, but a server that only answers in Brotli will return a body your code cannot read. Look for extras such as httpx[brotli] that would reinstall the package on the next build.
Python from a distribution package may take the fix as a system libbrotli update rather than a pip release. Check what the interpreter links before you decide you are done.
The same decoder elsewhere
Every product that carries its own copy of Brotli has to ship its own fix. Bun did so in 1.4.4. Envoy fixed it in 1.40.0, 1.39.3, 1.38.6 and 1.37.8, and Istio in 1.29.9, 1.30.6 and 1.31.2. curl builds linked against Brotli decode it when you pass --compressed, and Git builds whose libcurl supports Brotli ask for it on HTTPS fetches. Git for Windows 2.56.0.windows.3 still bundled Brotli 1.2.0 on 24 October.
On Debian, Node.js and Python packages from the distribution link the system Brotli library, so the fix arrives through the brotli package: 1.0.9-2+deb12u1 for bookworm and 1.1.0-2+deb13u1 for trixie.
Why the scanners stayed quiet
The bug sits in a native library below your application. Static analysis reads your source, and dependency scanners read manifests and lockfiles. Neither sees a decoder compiled into a Node binary or a Python wheel.
An ordinary request reaches it. With the body parser mounted globally, as it usually is, the vulnerable code runs before your route handler and your authentication middleware.
The remediation is a version bump plus a restart or a rebuild. Application code does not change, which makes the fix look easy until you have to prove that the process answering traffic runs the new version.
Google committed the change in February and published releases in October. A commit in a repository does nothing for the binaries already deployed, and no advisory pointed at it for eight months.
The ten-minute check
Read the version from the binary that runs in production. A manifest only tells you what someone meant to deploy.
node --versionnode -p "require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH"Compare the first line with 22.23.4, 24.22.1 and 26.12.1. The second line prints -17 on a fixed build and undefined on a vulnerable one, because the fix added that error code.
Skip process.versions.brotli for this. Node.js cherry-picked the upstream commit without bumping the bundled version, so a patched 22.23.4 still reports 1.1.0 and a patched 24.22.1 still reports 1.2.0. The string looks the same before and after.
In containers, run the same commands against the image you deploy:
docker run --rm --entrypoint node <image> --versiondocker run --rm --entrypoint node <image> -p "require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH"Distribution-packaged Node.js may link the system Brotli library. Then the decoder fix arrives with the distribution’s brotli package, and the Node version number tells you nothing about it:
ldd "$(command -v node)" | grep -i brotliFor the request-body path:
npm ls express body-parsergrep -rnE "express\.json|express\.raw|bodyParser|brotliDecompress|createBrotliDecompress" --include=*.js --include=*.mjs --include=*.ts .For Python, ask the interpreter that serves production traffic:
python -c "import brotli; print(brotli.__version__)"python -c "import brotlicffi; print(brotlicffi.__version__)"pip show Brotli brotlicffipipdeptree -r -p Brotlipip-auditAn ImportError on either import means that package is not installed in that environment. pipdeptree shows which dependency pulled Brotli in.
If the check turns up a host you cannot patch this week, or you cannot say which runtime a service runs, book a free 30-minute call. We go through the results with you and sort the hosts by exposure.
What DevOps should do this week
Patch in the order of exposure. Start with internet-facing services that decode Brotli from untrusted sources, then API gateways and reverse proxies that terminate compression, then anything that fetches URLs on behalf of users. Internal batch jobs come last.
Move image tags and rebuild. A rebuild that reuses a cached layer with the old runtime changes nothing, so verify the version inside the new image before it reaches a registry.
Restart the process. A patched package on the host does not update a container that started last week, and Node keeps the vulnerable decoder in memory until the process exits.
Confirm the running version on the deployed instance, not on your laptop, using the same one-liner.
When the upgrade cannot land today, reduce exposure while you plan it. Reject Content-Encoding: br at the edge, matching br case-insensitively and inside lists such as gzip, br. In Express, express.json({ inflate: false }) refuses compressed bodies until you upgrade; it rejects gzip bodies too, so test your clients first. Several WAF and hosting providers shipped rules for this CVE, and they cover inbound traffic only.
Outbound calls need a different fix. Removing br from Accept-Encoding does not help, because fetch(), urllib3 and curl decode whatever the response header announces. Route outbound traffic through an egress proxy that rejects responses with Content-Encoding: br, or turn decoding off where the client allows it, as urllib3 does with decode_content=False. For Python, uninstalling the two packages is faster than a proxy change and costs some bandwidth. Each of these buys a few days; the upgrade still has to follow.
What SecOps should do this week
Control the edge first. A rule on Content-Encoding: br costs one config line and holds until the runtime upgrade lands. Test it against legitimate clients before you switch it to blocking mode, because some SDKs and mobile apps send Brotli bodies.
Restrict egress from application servers. A compromised process that cannot download a second stage or open a tunnel is worth far less to an attacker. An allowlist breaks hobby projects; it fits most production services.
Harden the process, since exploitation lands inside a web worker or a Python interpreter. Run containers as non-root with a read-only root filesystem, drop Linux capabilities, apply a seccomp profile and leave the shell out of the image. None of this stops the memory corruption, but it raises the cost of every step after it.
Then watch for behaviour that follows a successful hit:
| Signal | Where to look | Why it matters |
|---|---|---|
node or python spawning sh, curl, wget, nc or base64 |
EDR, auditd | First move after code execution |
| Worker exits or restarts during decompression | Container logs, supervisor | The decoder aborting on malformed input |
| New outbound connections from app servers | Firewall, flow logs | Payload download, command and control, tunnelling |
Executables in /tmp, new listening ports, new cron entries |
EDR, file integrity monitoring | Persistence |
| Sudden interest in metadata endpoints or internal hosts | Egress logs, DNS | Credential theft from the host |
No single signal proves exploitation. Correlate them with the deployed runtime version and with the source of the compressed traffic.
Set deadlines by exposure first and by score second:
| Situation | Deadline |
|---|---|
| Internet-facing, decodes Brotli from untrusted sources | 72 hours |
| Internet-facing, Brotli reachable only through trusted upstreams | 7 days |
| Internal, untrusted input reaches the decoder | 14 days |
| Internal, no untrusted input, compensating controls in place | Next maintenance window |
| End-of-life runtime | Isolate now, migration date in writing |
One more rule for the week: do not run binaries that arrive with a vulnerability name attached. Fake scanners and proof-of-concept archives spread alongside every high-profile flaw. Fetch tooling from the Node.js release page, PyPI or the project’s own repository.
What the GDPR adds
If attacker code runs inside a process that handles personal data, treat the incident as a possible personal data breach. Article 33 GDPR requires you to notify your supervisory authority without undue delay and, where feasible, within 72 hours of becoming aware of a breach, unless it is unlikely to put people’s rights and freedoms at risk. Article 32 asks for security measures appropriate to the risk, taking the state of the art into account. Once a fix for a remote code execution flaw is public, running the old build gets hard to defend.
Write down when you patched each system and why any of them waited. That record answers the first question your data protection officer or an auditor will ask. Your DPO makes the legal call; this section is not legal advice.
What continuous monitoring has to cover
A quarterly scan answers the question “were we vulnerable in March”. This flaw needed an answer the same week, and the fix had been in the upstream repository since February.
Continuous monitoring rests on four pieces. Inventory has to cover images, hosts and CI runners, because a lockfile does not tell you which runtime answers traffic. Exposure mapping records which services accept or fetch compressed data from outside. Verification after every deploy turns the running version into a fact. Drift detection on base image tags catches the pin that nobody updated.
Someone also has to read the source advisories. Node.js publishes to the nodejs-sec mailing list, and Python packages land in PYSEC and OSV, sometimes a day after the release. If your only input is a scanner dashboard, you will hear about the next one late.
What we handle
We keep runtimes under watch for our clients. Our support contracts cover patch management for your stack, Node.js, Python and PHP included, monitoring for the services that depend on them, and restore checks on the backups. A written procedure covers the day a release like this one lands. Every patch gets a date and a reason in the log, and that log is what your GDPR documentation needs.
Go, Java, Ruby and .NET services get the same three steps: inventory what answers traffic, compare it with the fixed release, then prove the fix on the running process.
If you are not sure which of your servers run an affected build, book a free 30-minute call. We go through the runtimes on your servers with you, whatever language they are written in, tell you which ones are exposed and what to patch first. You do not need to send us an inventory in advance; a rough count of servers and services is enough to start. More on how we work is on our services page.
Is your runtime patched?
Free 30-minute patch check for your servers, in any language. We reply within one business day.
Sources
- Node.js: Wednesday, October 21, 2026 Security Releases
- Node.js v24.22.1 release notes (Brotli cherry-pick)
- CVE record CVE-2026-111497 (Google, CNA)
- GitHub Advisory GHSA-9v8h-qqgm-4m35
- OSV: PYSEC-2026-4323 (brotlicffi)
- Google Brotli v1.2.1 release
- brotlicffi on PyPI (changelog for 1.2.1.0)
- Imperva: Node2Shell, From Brotli Data to Remote Code Execution
- Debian security tracker: CVE-2026-111497
- expressjs/body-parser README (inflate option)