Direct answer: CVE-2026-111497, also called Node2Shell or Brotli2Shell, is an out-of-bounds write flaw in the Brotli decoder 1.0.3 to 1.2.0. An app that opens Brotli data from outside can crash, leak memory, or run attacker code. CISA KEV added the flaw to its list of exploited flaws on 27 October 2026. Update Node.js today to 22.23.4, 24.22.1, or 26.12.1.
Node.js is not the only affected product. The same decoder is in Bun, the Python packages brotli and brotlicffi, .NET 8 to 11, Envoy, Istio, and the libbrotli1 package on Debian and Ubuntu.
Main condition: Node.js from nodejs.org, nvm, and the official Docker image carries its own decoder, so you update Node.js. Node.js from Ubuntu and Debian apt uses the system library, so you update libbrotli1. Limit: The temporary steps in this article only reduce exposure. This article contains no exploit code.
We read the Node.js advisory of 21 October, the Brotli 1.2.1 release, the .NET advisory of 25 October, the PyPI advisory, the Ubuntu CVE page, the Debian tracker, and CISA KEV on 30 October 2026, at about 13:30 UTC. We ran the commands in steps 1 to 5 in podman containers with the official Node.js 24.22.0 and 24.22.1, Ubuntu 24.04, Python 3.13, .NET runtime 8.0.32 and 8.0.33, and nginx 1.29 images on the same date. The test server holds no customer data.
The problem: a Node.js update alone does not close every path
The flaw is in a library, not in one app. A server with an updated Node.js can stay vulnerable through Python, .NET, or the system library.
According to Tenable, Google put the fix in the Brotli source code on 27 February 2026. Node.js and Google released versions with the fix only on 21 October 2026. One day later, researchers ran code on the official Node.js 24.22.0 image. On 26 October, ELLIO recorded an exploitation attempt on Node.js 24.22.0 on x86-64.
There are 2 more traps. First, process.versions.brotli prints 1.2.0 on a vulnerable Node.js and on a patched Node.js. Our test in step 1 shows the same result as the ELLIO note. Second, a process that ran before the update keeps the old decoder until it restarts.
Check Point wrote on 29 October that a security team must not close the incident after a Node.js patch alone. The inventory must include runtimes, language packages, proxies, service meshes, containers, and software that carries its own Brotli library.

This month we also covered the PHP2Shell flaws and the GitLab CVSS 10 flaws. The same pattern came back: the gap from patch to attack attempt was only a few days. For this flaw, the gap was 5 days.
How it works: broken Brotli data makes the decoder write past a buffer
The decoder reads broken data as data that is not complete yet. As a result, the decoder keeps writing into a small buffer until it goes past its end.
This section uses the summary from the official advisories. Its purpose is to help you pick the right step, not to explain an attack.
Summary from the Node.js advisory
According to the Node.js advisory of 21 October, an invalid block switch was reported as a need for more input, not as a format error. The streaming decoder kept adding input bytes to an 8-byte internal buffer, then wrote past the end of that buffer (CWE-787). CISA names the flaw "Google Brotli Unchecked Return Value Vulnerability".
Node.js rates it High and writes that the flaw can allow remote code execution. The 22, 24, and 26 branches are affected. The 20, 23, and 25 branches are affected too, but their support has ended and they get no fix.
Scores from each body
Scores differ per source, so write each score with its source. NVD gives CVSS 3.1 9.8. Google as the CNA gives CVSS 4.0 9.2. Microsoft gives 8.1 for .NET, because Brotli decompression in .NET is off by default.

Exploitation status as of 30 October
CISA KEV added the flaw on 27 October 2026 based on evidence of active exploitation. ELLIO recorded 1 Brotli request of about 23 MB to Node.js 24.22.0 on 26 October, at 15:08 UTC. In 50 attempts in the ELLIO lab, that attacker chain did not produce a shell.
Check Point reports that 90.5% of the events in its telemetry carried Brotli-encoded HTTP payloads, and 24.5% targeted upload endpoints. Tenable has not independently confirmed the activity that ELLIO reported. The Singapore CSA asked for an immediate patch on 30 October 2026.
Who is most at risk
Apps that open Brotli-encoded request bodies or responses from outside parties are most at risk. MCPMCPA standard that lets an AI program use your tools and data under clear permissions.Open the glossary servers, chatbots, and APIAPIThe official door 2 systems use to exchange data, without anybody copying it by hand.Open the glossary backends on Node.js often use Express and fetch(), so they are in this group.
This table sums up Aikido, ELLIO, the .NET advisory of 25 October, and Tenable.
| How the app uses Brotli | Risk | What you update |
|---|---|---|
Express 5 or body-parser 2 accepts Content-Encoding: br bodies | High | Node.js |
Koa 3 with @koa/bodyparser 6 | High, tested by ELLIO | Node.js |
fetch() to user URLs, webhooks, scrapers, or AI agents | High, a proxy cannot stop it | Node.js |
zlib.brotliDecompress on uploaded files | High | Node.js |
| Python requests, urllib3, or httpx with the brotli package installed | High for outside URLs | pip brotli or brotlicffi |
| ASP.NET Core or HttpClient with br decompression on | When turned on | .NET runtime, republish NativeAOT |
| Envoy or Istio with the Brotli decompressor filter | When turned on | Envoy or Istio |
| Node.js from Ubuntu or Debian apt | Follows the system library | libbrotli1, then restart |

A plain node:http server does not open Brotli bodies itself, according to Aikido. Update anyway, because a package in node_modules can open them without your knowledge.
Versions that carry the fix
Update each product to the minimum version in this table or to a newer version in the same branch.
| Product | Minimum safe version | Note |
|---|---|---|
| Node.js 22 | 22.23.4 | Official binary, nvm, Docker image |
| Node.js 24 | 24.22.1 | Official binary, nvm, Docker image |
| Node.js 26 | 26.12.1 | Official binary, nvm, Docker image |
| Node.js 20, 23, 25 | No fix | Move to 22, 24, or 26 |
| Bun | 1.4.4 | Rebuild compiled executables |
| Google Brotli | 1.0.10, 1.1.1, 1.2.1 | Apps that bundle the library |
| pip brotli / brotlicffi | 1.2.1 / 1.2.1.0 | Every virtualenv and image |
| .NET 8 / 9 / 10 | 8.0.33 / 9.0.22 / 10.0.14 | Republish self-contained and NativeAOT apps |
| Envoy | 1.36.12, 1.37.8, 1.38.6, 1.39.3 | When the Brotli decompressor is on |
| Istio | 1.29.9, 1.30.6, 1.31.2 | When Brotli decompression is on |

For distro packages, use the versions from the Ubuntu CVE page and the Debian tracker. According to both sources, the apt nodejs package on Ubuntu 22.04 and newer and on Debian uses the system library, so a libbrotli1 update closes the flaw in that Node.js.
| Release | libbrotli1 with the fix |
|---|---|
| Ubuntu 22.04 LTS | 1.0.9-2ubuntu0.1 |
| Ubuntu 24.04 LTS | 1.1.0-2ubuntu0.1 |
| Ubuntu 26.04 LTS | 1.2.0-3ubuntu0.1 |
| Debian 12 (bookworm) | 1.0.9-2+deb12u1 |
| Debian 13 (trixie) | 1.1.0-2+deb13u1 |
PHP has a path too. According to Tenable, the official PHP builds for Windows are affected through the cURL extension when automatic decompression is on, with fixes in 8.2.35-1, 8.3.36-1, 8.4.27-1, and 8.5.12-1. PHP on Linux follows the system library or the PHP brotli extension, which has the fix in 0.21.1.
Prerequisites
- SSH access with
sudorights to the server, ordocker execaccess to the container. - A list of every place where Node.js, Python, and .NET run: servers, CI runners, container images, and serverless functions.
- Access to the app repo to change the Dockerfile,
package.json, andrequirements.txt. - A short restart window for each service.
- Read access to the nginx or load balancer logs, the systemd journal, and PM2.
When your Node.js servers hold tool access for AI agents, also read about access rights and audit logs for an MCP server. That guide explains how to limit what a compromised process can reach.
Step 1: Check the Node.js version on every server, CI runner, and container
Save this script as check-node.sh. The script reads the Node.js version and how it was built, then compares the version with the fix for its branch.
command -v node >/dev/null || { echo "Node.js is not installed on this machine."; exit; }
v=$(node -p process.versions.node)
if [ "$(node -p process.config.variables.node_shared_brotli)" = "true" ]; then
echo "$v: uses the system Brotli. Check libbrotli1 in step 2."; exit; fi
case ${v%%.*} in
22) fix=22.23.4;; 24) fix=24.22.1;; 26) fix=26.12.1;;
*) echo "$v: branch without a fix. Move to Node.js 22, 24, or 26."; exit;;
esac
lt() { [ "$1" != "$2" ] && [ "$(printf '%s\n' "$1" "$2" | sort -V | head -1)" = "$1" ]; }
if lt "$v" "$fix"; then echo "$v: VULNERABLE to CVE-2026-111497. Update to $fix."
else echo "$v: has the Brotli fix (minimum $fix)."; fi
Run the script on the server, in running containers, and on images before deploy.
bash check-node.sh # server or CI runner
docker exec -i container-name bash < check-node.sh # running container
docker run --rm -i --entrypoint bash node:24.22.0 < check-node.sh # image before deploy

Alpine-based images do not include bash. For those images, run node -v, then compare the result with the version table. For Bun, run bun --version. Versions below 1.4.4 are vulnerable.
Check the result: The output says "has the Brotli fix". A "VULNERABLE" result means Node.js must update today. A "uses the system Brotli" result means you go to step 2 for libbrotli1.
Step 2: Check libbrotli1, Python packages, and .NET
Save this script as check-lib.sh. The script checks the system library on Debian and Ubuntu, the pip packages brotli and brotlicffi, and the .NET runtimes.
lt() { [ "$1" != "$2" ] && [ "$(printf '%s\n' "$1" "$2" | sort -V | head -1)" = "$1" ]; }
# 1. System Brotli library on Debian and Ubuntu
. /etc/os-release
case $VERSION_CODENAME in
jammy) fix=1.0.9-2ubuntu0.1;; noble) fix=1.1.0-2ubuntu0.1;; resolute) fix=1.2.0-3ubuntu0.1;;
bookworm) fix=1.0.9-2+deb12u1;; trixie) fix=1.1.0-2+deb13u1;; *) fix=;;
esac
cur=$(dpkg-query -W -f='${Version}' libbrotli1 2>/dev/null)
if [ -z "$cur" ]; then :
elif [ -z "$fix" ]; then echo "libbrotli1 $cur: check your distro advisory."
elif dpkg --compare-versions "$cur" lt "$fix"; then echo "libbrotli1 $cur: VULNERABLE. Update to $fix."
else echo "libbrotli1 $cur: has the fix."; fi
# 2. Python packages brotli and brotlicffi
for py in python3 python; do command -v $py >/dev/null && break; done
for pkg in brotli:1.2.1 brotlicffi:1.2.1.0; do
v=$($py -c "import importlib.metadata as m; print(m.version('${pkg%%:*}'))" 2>/dev/null) || continue
if lt "$v" "${pkg#*:}"; then echo "pip ${pkg%%:*} $v: VULNERABLE. Update to ${pkg#*:}."
else echo "pip ${pkg%%:*} $v: has the fix."; fi
done
# 3. .NET runtimes
dotnet --list-runtimes 2>/dev/null | while read -r name v rest; do
case $v in 8.*) fix=8.0.33;; 9.*) fix=9.0.22;; 10.*) fix=10.0.14;; *) continue;; esac
if lt "$v" "$fix"; then echo "$name $v: VULNERABLE. Update to $fix."
else echo "$name $v: has the fix."; fi
done
bash check-lib.sh # server
source .venv/bin/activate && bash check-lib.sh # Python virtualenv
docker exec -i container-name bash < check-lib.sh # container

The script prints a line only for the components on that machine. For the RHEL family, run rpm -q brotli and compare it with your distro advisory. For Envoy and Istio, run envoy --version or istioctl version, then compare it with the version table.
Self-contained and NativeAOT .NET apps carry the decoder inside the app file. According to the .NET advisory of 25 October, a runtime update on the machine does not fix those apps. Republish the app with the patched packages.
Check the result: Each line says "has the fix". Run the script in each Python virtualenv, because each virtualenv has its own pip packages.
Step 3: Update, then restart every process
Update the vulnerable components, then restart every process that uses them. Use the command that matches how you installed each component.
# Node.js through nvm
nvm install 24.22.1 && nvm alias default 24.22.1
# Dockerfile: FROM node:24.22.1-bookworm-slim, then rebuild with the new base image
docker build --pull -t app:patch .
docker compose up -d --force-recreate
# System library on Debian and Ubuntu
sudo apt update && sudo apt install --only-upgrade libbrotli1
# Python packages
pip install -U 'brotli>=1.2.1' 'brotlicffi>=1.2.1.0'
# Bun
bun upgrade
Use an image tag with the full version, or rebuild with --pull so that the base image is new too. Aikido notes that an image with an old tag, for example node:24.22.0, stays vulnerable until you rebuild it. For .NET, install runtime 8.0.33, 9.0.22, or 10.0.14, then republish self-contained apps.
After a system library update, save this script as check-restart.sh. The script finds processes that still use the old Brotli library from memory.
n=0
for m in $(grep -l "libbrotli.*(deleted)" /proc/[0-9]*/maps 2>/dev/null); do
p=${m#/proc/}; p=${p%/maps}; n=$((n + 1))
echo "PID $p still uses the old Brotli: $(tr "\0" " " < /proc/$p/cmdline)"
done
echo "Processes to restart: $n"
sudo bash check-restart.sh
sudo systemctl restart service-name # systemd service
pm2 reload all # PM2 apps


The script sees only the system library. Official Node.js carries the decoder inside the node binary, so restart every Node.js service after the update. Serverless functions and hosting platforms need a new deploy so that the build uses the new runtime.
Check the result: Repeat steps 1 and 2. Every line says "has the fix", and check-restart.sh says "Processes to restart: 0".
Step 4 (optional): Reject Brotli bodies for a short time when you cannot update yet
Use this step only for a short gap until the update is done. The Node.js advisory of 21 October gives no workaround, and Tenable writes that a proxy block does not protect responses that the app fetches from outside servers.
In nginx, reject requests with the Content-Encoding: br header. Put the if line in the server block that accepts API calls and uploads, then run sudo nginx -t and sudo systemctl reload nginx.
server {
listen 80;
# Reject Brotli bodies until every app behind the proxy is patched.
if ($http_content_encoding ~* "\bbr\b") { return 415; }
location / { return 200 "ok\n"; }
}
In Express, reject Brotli bodies before express.json() and other parsers open them.
// Reject Brotli bodies before the body parser opens them.
app.use((req, res, next) => {
if (/\bbr\b/i.test(req.headers['content-encoding'] || '')) return res.status(415).send('Brotli not accepted');
next();
});
app.use(express.json());

In .NET, the .NET advisory of 25 October asks you to remove the "br" entry from DecompressionProviders and to limit AutomaticDecompression. Microsoft also asks you not to add new decompression settings where none exist.
// ASP.NET Core: remove "br" from the existing DecompressionProviders.
// HttpClient: open only GZip and Deflate.
var handler = new HttpClientHandler {
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
};
In Python, Tenable advises you to remove the brotli and brotlicffi packages until the new version is installed. Without those packages, urllib3 and requests do not open Brotli responses. In Envoy and Istio, turn off the Brotli decompressor filter in both directions. Removing br from the Accept-Encoding header alone does not close the path.
Check the result: A request with Content-Encoding: br gets HTTP 415. A request without Brotli still gets HTTP 200.
Step 5: Look for signs that the server was touched
A failed attack often crashes the process. A successful attack can leave no trace in the logs. Save this script as check-signs.sh and run it with sudo on the nginx server.
log() { zcat -f /var/log/nginx/access.log*; }
echo "ELLIO IoC : $(log | grep -cE "node2shell-check|94\.154\.43\.7")"
echo "Rejected 415 : $(log | awk "\$9 == 415" | wc -l)"
echo "5xx errors : $(log | awk "\$9 >= 500" | wc -l)"
Then check crashes, restarts, and child processes for the period since 21 October 2026.
sudo journalctl --since 2026-10-21 | grep -ciE 'node.*(segfault|sigsegv|sigabrt)|core dumped'
coredumpctl list node --since 2026-10-21
pm2 list # the ↺ column is the restart count
docker inspect -f '{{.Name}} restart={{.RestartCount}}' $(docker ps -q)
ps -eo pid,ppid,user,etime,cmd --forest | grep -A3 '[n]ode'


Look for 4 patterns from ELLIO and Check Point. First, node processes that crash, restart loops, or new core dumps. Second, child processes such as sh, bash, or curl under a node process.
Third, large or broken Brotli bodies, mainly to upload and API endpoints. Fourth, IoC from ELLIO: the user agentAI agentAn AI program that performs work steps by itself, for example reading a message, drafting a reply, and recording the result.Open the glossary node2shell-check, the address 94.154.43.7, and the error ERR__ERROR_FORMAT_BLOCK_SWITCH on a patched version. That error shows that someone is testing your server.
Check the result: The IoC count is 0, there are no crashes or restart loops, and there are no child processes that you do not know. When you see a sign, isolate the server and keep the logs. Limit access to core dumps, because ELLIO writes that they can hold session data. Replace every secret the node process can read: .env values, API keys, database tokens, and cloud tokens.
Example: 1 evening of response for 4 services
This simulation uses dummy data. The service names, times, and results are not customer data.
| Time (UTC) | Dummy service | Check | Result | Action |
|---|---|---|---|---|
| 12:00 | shop-api (Docker node:24.22.0, Express 5) | Step 1 | VULNERABLE | Step 4 in nginx, then rebuild on 24.22.1 |
| 12:40 | wa-bot (nvm Node.js 22.23.3, webhookWebhookAn automatic message one system sends to another the moment something happens, for example an incoming WhatsApp message.Open the glossary fetch) | Step 1 | VULNERABLE | nvm install 22.23.4, pm2 reload all |
| 13:10 | price-scraper (Python 3.12, brotli 1.1.0) | Step 2 | pip brotli VULNERABLE | pip install -U brotli, restart the worker |
| 13:30 | old-web (Ubuntu 24.04, Node.js from apt) | Steps 1 and 2 | libbrotli1 VULNERABLE | apt upgrade, check-restart.sh, restart 2 services |
| 14:00 | shop-api | Step 5 | 3 restarts in a row on 26 October | Isolate, keep logs, replace secrets |
The lesson from the simulation: the nginx rule does not protect a service that fetches outside URLs. Restart loops before the patch need an investigation, not only an update.
Who installs the patch
The patch duty depends on the hosting type. This table helps you see who must act today.
| Hosting type | Patch owner | What you do |
|---|---|---|
| Own VPS or server | Your IT team | Steps 1 to 5 |
| Container or Kubernetes | The team that owns the images | Change the base image, rebuild, then steps 1 and 5 |
| Serverless or hosting platform | The platform and your team | Read the platform notice, then deploy again |
| App run by a vendor | The vendor | Ask for written proof of the version, then the step 5 results |
Rama Digital recommendation: when your Node.js server holds payment tokens, WhatsApp tokens, or AI model API keys, treat every sign in step 5 as an incident. For an MCP server, also read about access rights, approval, and audit logs for an MCP server.
Checklist for today
- List every place where Node.js, Bun, Python, and .NET run. Owner: DevOps team. Evidence: a list of hosts, images, and CI runners.
- Check Node.js with step 1. Owner: server admin. Evidence: script output per host and per image.
- Check libbrotli1, pip, and .NET with step 2. Owner: server admin. Evidence: script output per host and per virtualenv.
- Update and rebuild images. Owner: developer. Evidence: the new Dockerfile and lockfile.
- Restart every process. Owner: server admin. Evidence:
check-restart.shoutput says 0. - Apply step 4 on services that cannot update yet. Owner: nginx admin. Evidence: the HTTP 415 test.
- Look for signs with step 5. Owner: server admin. Evidence: command output, kept with the logs.
- Stop when every component says "has the fix" and step 5 is clean. When you see a sign, stop the patch work and start incident response.
Frequently asked questions
I do not use Express. Do I still need to update? Yes. fetch() opens Brotli responses on its own, and other packages in node_modules can open Brotli bodies. A Node.js update is the only complete fix.
process.versions.brotli prints 1.2.0. Is my Node.js safe? Not always. The vulnerable Node.js 24.22.0 and the patched 24.22.1 both print 1.2.0. Check the Node.js version with step 1.
My Node.js comes from Ubuntu apt. What must I update? Update the libbrotli1 package, then restart the Node.js services. Ubuntu writes that the nodejs package on 22.04 and newer uses the system library.
Does a Content-Encoding br block in nginx close the flaw? No. The block stops request bodies from outside. Responses that the app fetches from outside servers are still opened by the decoder.
I still use Node.js 20. When does the fix come? There is no fix. Node.js 20, 23, and 25 are past their support. Move to Node.js 22, 24, or 26.
Is my PHP or WordPress site affected? It can be, through the system library or the PHP brotli extension. Update libbrotli1 and the brotli extension to 0.21.1, then restart PHP-FPM.
How do I know whether the server was broken into? Look for crashes, restart loops, odd child processes, and the IoC in step 5. Clean logs do not prove that the server is safe.
Next step
The limit that still applies: an update closes this flaw, but it does not remove access that an attacker may have planted before the patch. When your services show a sign in step 5, or you need help to check images and secrets after an incident, use Vibe Code Rescue for an audit and stabilisation. For 1 urgent issue today, Book a 30-minute session.
Sources
- Node.js advisory of 21 October: flaw summary, affected branches, and fixed versions.
- Brotli 1.2.1 release: the library release with the fix.
- CVE-2026-111497 and NVD: CVE record, CWE-787, scores 9.2 and 9.8.
- CISA KEV: CVE-2026-111497 added to KEV, 27 October 2026.
- .NET advisory of 25 October: .NET versions, the 8.1 score, and mitigations.
- PyPI advisory: brotli and brotlicffi versions.
- Ubuntu CVE page and Debian tracker: libbrotli1 versions and the nodejs package status.
- ELLIO: the 26 October exploitation attempt, IoC, and detection signs.
- Aikido: exposed Node.js APIs and the Bun version.
- Tenable: timeline, other products, and temporary steps.
- Check Point: telemetry of 29 October and the response order.
- Singapore CSA: alert of 30 October 2026.




