Node2Shell (CVE-2026-111497) Emergency Response Plan

Node2Shell allows server takeover without authentication. Please check the methods for verifying the vulnerability and remediation measures that were directly reproduced and verified.
Xint's avatar
Nov 09, 2026
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

CVE-2026-111497

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
(22.x less than 22.23.4, 24.x less than 24.22.1, 26.x less than 26.12.1)

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)
Fixed by: mcollina · aduh95

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 → Unpatched

Node version

process.versions.brotli

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 patched

For 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 Content-Encoding: br. The inflate option defaults to true, so it applies without any extra configuration.

Global fetch()

✓

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 br with a 415.

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

  1. Enter the service URL to check, including https://.

  2. When the check finishes, review the result and recommended actions shown on screen.

  3. If you run multiple domains, subdomains, or API endpoints, check each one separately.

  4. 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

  1. Update 22.x / 24.x / 26.x to 22.23.4 / 24.22.1 / 26.12.1 or higher.

  2. 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.

  3. For containers, rebuild the base image and redeploy. Tags like node:22-alpine can point to different images over time, so don't rely on the tag alone. Verify the version directly inside the new image.

  4. 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 Content-Encoding: br request bodies, such as those running Express 5

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/null

The 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

ERR__ERROR_FORMAT_BLOCK_SWITCH (errno -17)

Patched. The decoder blocked successfully.

ERR__ERROR_FORMAT_BLOCK_LENGTH_1 (errno -9)

Unpatched. An error occurred belatedly after passing an incorrect block switch.

Z_BUF_ERROR (errno -5)

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 node

Also 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