Node2Shell (CVE-2026-111497) Emergency Response Plan
1. Node2Shell Summary
This is a vulnerability in Brotli decoders bundled with or dynamically linked to Node.js. Node.js has classified it as an out-of-bounds write that can lead to remote code execution. It is affected solely by the Node version, regardless of the application code. However, the mere fact of using Node does not constitute a remote attack. An unpatched decoder and a path through which attacker-controlled Brotli data reaches the decoder must coexist. Typical entry points include Express 5’s body-parser and Node’s global fetch().
Item | Detail |
CVE | |
Severity | HIGH - Node.js Official Rating. “Brotli decoder out-of-bounds write permits remote code execution” |
Fixed version | 22.23.4 / 24.22.1 / 26.12.1 (Released on 2026-10-21) |
Impact | Node process and worker crashes, denial of service due to CPU exhaustion, conditional memory exposure, and remote code execution |
Affected versions | 22.x, 24.x, 26.x below the fixed version |
Node 20.x or lower | No official release. You need to check for security updates from your distribution (Ubuntu, Debian, RHEL, etc.) or LTS support vendor. |
Cause | The decoder fails to filter out invalid block switches and continues decoding. Fixed in upstream brotli v1.2.1. |
Mitigation | A patch is the only fundamental measure, and until it is applied, a temporary mitigation that blocks the Brotli decompression path can buy time for a response. |
Reported by / Fixed by | Reported by: OpenAI Codex Security (Khai Tran, Sylvie Mayer) |
The same security release includes three more HIGH-rated issues: a NUL-character handling bug in dns.resolve (CVE-2026-105482) and two TLS certificate hostname bypasses (CVE-2026-105483, CVE-2026-106069).
2. Methods to identify vulnerable environments
You must verify two things:
If the decoder used by the actual service is unpatched
If a path exists for an attacker to reach the decoder.
If both conditions exist, a remote attack is actually possible.
2.1 Runtime Patch Status
The Node.js team added a new constant, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH, as part of the patch.
If it exists at runtime, you can assume the patch has been applied.
node -p "require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH ?? 'ABSENT'"
# -17 → Patched
# ABSENT → UnpatchedNode version |
| Constant marker | Verdict |
22.23.3 | 1.1.0 | Absent | Vulnerable |
22.23.4 | 1.1.0 | -17 | Patched |
24.19.0 | 1.2.0 | Absent | Vulnerable |
24.22.1 | 1.2.0 | -17 | Patched |
26.12.0 | 1.2.0 | Absent | Vulnerable |
26.12.1 | 1.2.0 | -17 | Patched |
On distribution-packaged Node (--shared-brotli builds), the constant alone is not enough to determine whether the decoder is patched. The constant's value comes from the Brotli headers used at build time, while the actual decoder code lives in libbrotlidec.so.1, which is loaded at runtime.
First, run the command below to check whether your Node was built with the --shared-brotli option.
node -p "process.config.variables.node_shared_brotli"If node_shared_brotli is true, check both the currently loaded Brotli version and the distribution package.
node -p "process.versions.brotli"The shared-brotli build takes action based on the following criteria.
Brotli version conditions | Verdict | Action |
1.0.x < 1.0.10 / 1.1.x < 1.1.1 / 1.2.x < 1.2.1 | Vulnerable | Restart the Node process after updating to the fixed package |
1.0.10+ / 1.1.1+ / 1.2.1+ | Patched | Re-verify after restarting the Node process. |
process.versions.brotli reports the version of the encoder (libbrotlienc), but the vulnerability is in the decoder. The two are shipped together from the same source package, so their versions usually match. To be certain, though, it's best to check the decoder directly.
# Check the library only, without running Node
strings /usr/lib/x86_64-linux-gnu/libbrotlidec.so.1 | grep -c ERROR_FORMAT_BLOCK_SWITCH
# Any non-zero result means it's patchedFor bundled builds, check the node binary the same way. This also works when you need to inspect a registry image without running the container.
2.2 Verify Exposure Path
Don’t skip this just because “we don’t use Brotli.” Even if Brotli appears nowhere in your code, the decoder still runs in the cases below.
Channel | Verdict | Condition |
Express 5 request body | ✓ | Since body-parser 2.0.0, it decompresses |
Global | ✓ | This always applies to Node 18 or higher. Since the Brotli in the response is automatically uncompressed, it is exposed regardless of dependencies. |
HTTP client | ✓ | axios, got, node-fetch, superagent, undici, decompress-response, etc. automatically decompress the response. |
Express 4 request body | X | body-parser 1.x only accepts gzip and deflate, and rejects |
In Xint's own testing, all four Express 5 parsers — express.json(), express.urlencoded(), express.text(), and express.raw() — decompress Content-Encoding: br in the same way for any request with a matching Content-Type.
Outbound paths can also be attack vectors. This covers any path where the server reads a response from an external URL, such as webhook handling, external API integrations, and fetching feeds or images.
2.3 Online Inspection Service
You can check the possibility of Node2Shell exposure on the page below without downloading a separate script or running it on a server.
👉 Go to Node.js Guard: https://nodejsguard.io
Enter the service URL to check, including
https://.When the check finishes, review the result and recommended actions shown on screen.
If you run multiple domains, subdomains, or API endpoints, check each one separately.
After patching or changing settings, enter the same URL again to see whether the result has changed.
⚠️
Only enter services you own or are authorized to test.
The web check only covers what is reachable from the outside. For internal-only services, origins that can't be reached directly from the internet, and Node environments without a URL (such as batch jobs or workers), check the runtime directly using the method in 2.1, and separately review your container images, your distribution's security advisories, and your application configuration.
3. Remediation
A patch is the only fundamental solution. The temporary mitigation below is a measure to buy time to respond until the patch is applied.
3.1 (P0) Apply the Patch
Update 22.x / 24.x / 26.x to 22.23.4 / 24.22.1 / 26.12.1 or higher.
After updating, be sure to restart the process. For services run with PM2 or systemd, the previous process remains intact even if the binary is replaced.
For containers, rebuild the base image and redeploy. Tags like
node:22-alpinecan point to different images over time, so don't rely on the tag alone. Verify the version directly inside the new image.After deployment, re-verify the actual service node using the method in 2.1.
There are no official upstream security releases for end-of-life (EOL) lines such as Node 20 and earlier, or 21, 23, and 25. Brotli support landed in Node core in 10.16.0 and 11.7.0, so every EOL version from that point on is affected. First check whether your distribution or an LTS support vendor provides security updates. If not, the rule is to migrate to a supported version.
Patch priority should be based on how each system is exposed.
Priority | Target |
1 | Services that read external responses via webhooks, external API integrations, or paths reachable through SSRF |
2 | Services that accept |
3 | Everything else, on the normal patch schedule |
3.2 (P1) Temporary Mitigations Until Patched
This is a method to block the path for an attacker to reach the Brotli decoder.
Blocking the request body — the method of rejecting it in a reverse proxy is the clearest.
# nginx: reject request bodies encoded with br
if ($http_content_encoding ~* "(^|,)\s*br\s*($|,)") { return 415; }If the application blocks it, it must be placed in front of body-parser.
app.use((req, res, next) => {
const ce = String(req.headers['content-encoding'] || '').toLowerCase();
if (ce.split(',').some((e) => e.trim() === 'br')) return res.sendStatus(415);
next();
});⚠️
We don't recommend turning off inflate with express.json({ inflate: false }). It also blocks gzip and deflate, which can break normal functionality.
3.3 Checking for Compromise
Because the vulnerability is triggered during decompression, it shows up in application logs as a “bad request.” Even a failed attack leaves a trace.
① Surge in Brotli decoding errors
This is an error that rarely occurs in normal traffic. If it spikes, you may suspect a reconnaissance scan or an attempted attack.
grep -cE "ERR__ERROR_FORMAT_|Z_BUF_ERROR|BrotliDecoder|unexpected end of file" \
/var/log/app/*.log 2>/dev/nullThe error codes differ before and after the patch. By examining which one was recorded in the log, you can also verify the runtime state at that time.
Values logged | Runtime state |
| Patched. The decoder blocked successfully. |
| Unpatched. An error occurred belatedly after passing an incorrect block switch. |
| May indicate an unpatched runtime. Since it occurs even if the request is interrupted midway, please also check if it is repeating from the same IP. |
② Abnormal Worker Termination and Response Delay
# Container OOM kills and restarts
docker ps -a --format '{{.Names}}\t{{.Status}}' | grep -iE "restart|exited"
kubectl get pods --field-selector=status.phase!=Running
# Traces left by the kernel
dmesg | grep -iE "oom|segfault" | grep -i nodeAlso be suspicious if you see an increase in unexplained worker restarts, or a pattern of responses stalling right after Content-Encoding: br requests. Async zlib runs on the libuv thread pool, so if one thread gets stuck, unrelated operations like fs or dns can slow down too.
4. References
Node.js October 2026 Security Release: https://nodejs.org/en/blog/vulnerability/october-2026-security-releases
Node.js v22.23.4 Release: https://nodejs.org/en/blog/release/v22.23.4
Node.js v24.22.1 Release: https://nodejs.org/en/blog/release/v24.22.1
Node.js v26.12.1 Release: https://nodejs.org/en/blog/release/v26.12.1
upstream brotli fix commit: https://github.com/google/brotli/commit/0ed4268a4401abcb4472096340477cf4af7d1a2b
Node.js deps cherry-picked commit: https://github.com/nodejs/node/commit/8c5debbea41dd50ed5b5bb9faf8ef442c5a794e4
Node.js Constant Exposure Commit: https://github.com/nodejs/node/commit/a18ec92752cd05ad5335bfaa5420a258b81f9afc