InfinitumIT
Analysis Report

Remote Code Execution Flaw in the Node.js Brotli Decoder (CVE-2026-111497)

CVE-2026-111497, a flaw in the Brotli decoder embedded in Node.js, affects every application that decompresses attacker-controlled compressed data. We summarise the affected versions, a one-line patch check, mitigations and detection guidance for SOC teams.

22.10.2026 · 12 min read · InfinitumIT
Remote Code Execution Flaw in the Node.js Brotli Decoder (CVE-2026-111497)

The Node.js team has fixed, across all three supported release lines, a critical flaw (CVE-2026-111497) that affects the Brotli decoder embedded in the runtime and can reach any application that decompresses attacker-controlled compressed data. We have summarised the affected versions, why this is not "just another library bug" and detection guidance for SOC teams.

CVE-2026-111497 · Out-of-bounds write (CWE-787) · Severity: High · Affected: Node.js 22.x / 24.x / 26.x and all EOL lines · 22.10.2026

On 21 October 2026, the Node.js team released a security update for all of its supported lines: 22.x, 24.x and 26.x. The update fixes ten separate flaws; the most serious is a memory write bug in the Brotli compression library that Node.js bundles internally, which, in the project's own words, allows remote code execution. What sets this bug apart is that it lives not in an application's code but inside the runtime itself. So what the application does, or which framework it is written with, does not matter; it is enough for the application to decompress Brotli data sent by an attacker at some point. This article brings together the technical details of the flaw, the code paths at risk and the measures that need to be taken.

History of the flaw and the patch process

The origin of the fix goes back before 21 October. On 27 February 2026, a commit titled "Detect and report invalid stream earlier" landed in Google's Brotli project. The commit was not labelled as a security fix and carried no CVE number; from the outside it looked like a routine improvement that tightened the decoder's error handling, and it stayed that way for eight months.

Figure 1. From the upstream hardening commit to the security release.
Figure 1. From the upstream hardening commit to the security release.

Things changed once it became clear that the behaviour this commit closed could lead to memory corruption. The Node.js security team backported the fix to all three lines through a private security PR, and on 21 October 2026 the patch was released as CVE-2026-111497. On the same day Google also shipped Brotli 1.2.1 as a security release (together with 1.1.1 and 1.0.10 for older lines). This detail matters: every piece of software outside Node.js that bundles its own copy of Brotli needs the same update separately.

On the Node.js side the flaw was reported by Matteo Collina and the fix was prepared by Antoine du Hamel; the upstream commit is by Evgenii Kliuchnikov of Google. Node.js rates this flaw as HIGH, not CRITICAL. Since the CVE record and NVD entry were not yet public at release time, there was no official CVSS vector.

Where exactly is the flaw?

Brotli splits the data in a stream into different "block types": literal blocks, command (insert/copy) blocks and distance blocks. The decoder can switch between these types in the middle of a stream, and how many block types are defined and where the switches happen are encoded in the stream itself. In a normal stream this is consistent.

The problem arises in an inconsistent stream. If a stream defines only one block type but still requests a block switch, that is an invalid state. The decoder's DecodeBlockTypeAndLength function noticed this and returned a failure value. So far, so good. The real defect lies in what happened to that failure: the block switch wrappers on the fast decoding path (three separate functions for literals, commands and distances) were void functions that returned nothing. So the "this stream is invalid" signal produced by the function below was never passed up; it was silently swallowed. The decoder carried on decoding while its internal state (the block type ring buffer and the related Huffman tree indexes) was inconsistent, and this resulted in a write outside the intended buffer (an out-of-bounds write). This write happens in native code, directly in the memory space of the Node.js process.

What the patch does is precisely to remove this silent swallowing. DecodeBlockTypeAndLength now returns a real error code instead of a boolean; when a switch is requested while only one block type exists, the newly defined BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (error code -17) is produced. All three block switch wrappers now pass this error code up, and a new macro (BROTLI_SAFE_WITH_STATUS) stops decoding as soon as it sees the failure. In short, the fix turns "write and carry on" into "notice and stop". A second Node commit exposes this new constant through zlib.constants; as we will see shortly, this gives us a one-line test to verify patch status.

How wide is the impact?

Most web vulnerabilities depend on a particular plugin, a framework or badly written application code. This flaw is different, because the faulty code is not in the application but inside the runtime. The http module in the Node.js core does not decompress anything on its own; however, Brotli decoding kicks in through many paths that application code can trigger without explicitly asking for it. That is what makes the scope of this flaw so wide.

Figure 2. All the different entry paths reach the same native decoder inside the runtime.
Figure 2. All the different entry paths reach the same native decoder inside the runtime.

The first path is fetch(). Node's built-in fetch runs on undici, and when a response arrives with a Content-Encoding: br header it hands the body to the Brotli decoder. The critical point here is this: even if the client did not ask for Brotli in its request, it is the response header that decides how the response is decoded. So the server being called, or any party able to tamper with that call's traffic, can impose the encoding. This turns the flaw from a matter of incoming requests alone into a problem that also covers the calls the application makes outward.

On the incoming side, Express 5 stands out. Brotli support arrived with body-parser 2.0.0, and it decompresses request bodies by default. Because Express 5 depends on body-parser 2.x, express.json() and express.urlencoded() accept and decompress a Content-Encoding: br payload sent by anyone who can reach the route. axios also requests Brotli by default on the Node side and decompresses the response, so it falls into the same path. Add to this your own zlib.brotliDecompress() / createBrotliDecompress() calls (proxies, crawlers, webhook receivers, file upload pipelines) and upstream Brotli responses in self-hosted Next.js image optimisation.

Assessing the exploitation status

Sources differ on the demonstrated impact. Vercel's bulletin states that researchers demonstrated both memory disclosure and command execution on unpatched 22.x, 24.x and 26.x. By contrast, the proof of concept (PoC) published on 22 October achieved code execution only on a deliberately weakened test build without ASLR (non-PIE). On a standard Node.js build the same PoC resulted not in code execution but in a crash and a limited memory read; the PoC's author also states that they could not achieve a working attack on a normal build. The heap layout needed to hijack control flow has not been made public.

So at present there is no proven remote code execution exploit working in the wild against a standard Node.js installation. As of 22 October there are no attack reports, and the flaw is not in the CISA KEV catalog. Still, the risk should not be underestimated: there is a reliable remote crash (DoS) for every route that decompresses Brotli, and the risk of memory disclosure is real. Memory disclosure can leak sensitive data such as passwords, tokens and keys without code execution ever being reached. This is why the flaw should be patched as a priority, without waiting for the NVD score to be published.

Am I patched? A one-line check

Because the patch adds a new decoder error code and Node exposes that code as a constant, you can test directly without looking at version tables:

node -p "require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH"
# -17        => patched (22.23.4, 24.22.1, 26.12.1)
# undefined  => vulnerable

Run this check not on your laptop but inside every image you build and every CI runner. Your developer machine being on the right version says nothing about a base image pulled in August. After the patch, a process rejects a malformed stream with an error code; seeing this code in your logs means that someone sent a stream crafted that way, or that a server you call sent corrupt data. Both are worth looking into.

Which versions are affected, and which get a patch?

Node.js released new versions only for its supported lines. By rule, end-of-life (EOL) lines are always considered affected in security releases.

Version in use Upgrade to Status
26.x 26.12.1 Current
24.x 24.22.1 Active LTS
22.x 22.23.4 Active LTS
20.x and earlier No official patch End of life; move to the 22.x or 24.x line

Node.js 20 is not "out of scope" but "out of support". In other words, it remains vulnerable but will not receive a fix. In container environments you need to move to a patched tag (node:24.22.1-bookworm-slim and equivalents), rebuild and redeploy the image, and verify the running version inside the container with node -v. On managed platforms such as Vercel, the platform manages the patch version for projects running 22.x and 24.x; projects still pinned to Node 20, however, must move the version to 22.x or 24.x manually and redeploy.

The same update closes nine more flaws besides Brotli. Among them are two high-severity flaws that bypass TLS server name verification when an IP address is written in a non-canonical form or with fullwidth digits (CVE-2026-106069 and CVE-2026-105483), a flaw where dns.resolve truncates names at an embedded NUL byte (CVE-2026-105482), and a flaw that makes crypto.randomUUID() and randomInt() repeat the same values in startup snapshots (CVE-2026-106074). The release also blocks eight eMudhra intermediate CA keys reported to be compromised and updates the bundled copies of undici, OpenSSL, c-ares and SQLite. A single runtime update closes all of these.

What to do today

  1. Take inventory. List servers, container images, CI environments and developer machines. Do not overlook products that ship their own Node.js: the Electron layers of desktop applications, the management interfaces of some network appliances and Node-based enterprise tools are also in scope.

  2. Prioritise internet-facing systems that decompress Brotli. Start with services that decompress incoming request bodies (Express 5 / body-parser 2.x) and those that call external servers with fetch().

  3. Update and restart the processes. Installing the new binary alone is not enough. If processes running under PM2, systemd or container orchestration are not restarted, the old code keeps running in memory. In containers, images should be rebuilt on the new base image, and you should confirm that the tag in use really points to the patched version.

  4. Verify the result. After the update, confirm the running version with the zlib.constants check above, and if possible on the process that is actually handling traffic.

  5. Handle unsupported systems separately. Systems running Node 20 or earlier should be moved to a supported line. If that is not possible, take the system off the internet and restrict access to it.

  6. Do not forget other components that carry their own copy of Brotli. Python bindings, native modules and other runtimes need their own updates; the Node release only covers Node.

If the patch cannot be applied right away

The Node.js security advisory makes it clear that updating is the only complete fix. Still, it is possible to narrow the known entry paths until the patch is applied, but you need to know the limits of these measures.

On the incoming side, if Express/body-parser is used, you can pass { inflate: false } to the express.json(), express.urlencoded(), express.text() and express.raw() calls. body-parser will then reject compressed bodies with a 415 instead of decompressing them. Similarly, request bodies carrying Content-Encoding: br can be dropped at the proxy or load balancer level. WAF rules (Vercel published them for its own network on release day) also buy time, but they only protect traffic that passes through that service; if the server can be reached directly by IP, that protection is bypassed.

For outgoing calls, however, there is no clean workaround. Removing br from the Accept-Encoding header "asks" the server not to use Brotli, but because undici's fetch still decompresses whatever Content-Encoding the response claims, a malicious server can ignore that request. Only the runtime update makes fetched responses safe. And that sums up why the patch is the only real fix.

Monitoring guide for MDR and SOC teams

Patching does not undo a breach that has already happened. No official IOC list (IPs, hashes, signatures) has been published yet, so detection has to be built on behaviour. Moreover, because the attack happens below the application layer, in the runtime's native code, application logs will often stay silent. The real visibility will come from web server logs, Node process telemetry (crashes and restarts) and endpoint telemetry.

Visibility: which logs do you need?

Default access logs fall short for this flaw. In their standard format, Nginx and Apache do not record the request body size or the Content-Encoding header, yet these two fields are the easiest way to tell Brotli-based attempts apart. Adding them to the log format is worthwhile. Beyond that, make sure the crash/restart events of the Node process manager (PM2, systemd) and the container runtime, process creation records from EDR or auditd, and network data showing the web server's outbound connections are flowing into the SIEM. If a reverse proxy is in use, logging response headers may be the only way to see memory disclosure that could happen on the outbound side.

Detection scenarios

The table below brings together the signals expected at different stages of exploitation. The first rows cover the attempt and crash stage; the later rows cover the period after a successful breach.

Signal Data source Why it matters
An HTTP 500 or process crash right after a POST/PUT request with a Content-Encoding: br header Access log + Node/process manager log The known entry paths follow these two steps
Node workers crashing with SIGSEGV (signal 11), a sudden rise in PM2/systemd/container restarts PM2 log, journald, dmesg The clearest sign of memory corruption (and remote DoS)
Repeated HTTP 500 responses from the same source in a short time Access log, WAF, reverse proxy Attempts that rely on memory layout often need repetition
Abnormal/binary content returned from outgoing calls, unexpected Content-Encoding: br responses Reverse proxy/client logs Outbound memory disclosure channel
A shell, script interpreter or download tool spawned by a Node process EDR, auditd, Sysmon for Linux The clearest sign of successful code execution
Unexpected outbound connections from the web server Firewall, NetFlow, EDR network telemetry Reverse shell, C2 or data exfiltration
New or modified script files in the app directory, a new cron job/systemd service or account FIM, auditd, EDR Persistence and privilege escalation

Sample Sigma rule

After successful code execution, the most reliable signal is a Node process spawning a shell or a tool. The rule below is written for Linux process creation events and can be converted for the SIEM in use:

title: Shell or tool spawned by a Node.js process
status: experimental
description: Possible code execution after CVE-2026-111497
logsource:
    category: process_creation
    product: linux
detection:
    selection_parent:
        ParentImage|endswith:
            - '/node'
    selection_child:
        Image|endswith:
            - '/sh'
            - '/bash'
            - '/dash'
            - '/python3'
            - '/perl'
            - '/curl'
            - '/wget'
            - '/nc'
            - '/ncat'
            - '/socat'
    condition: selection_parent and selection_child
falsepositives:
    - Apps that legitimately spawn child processes via child_process
level: high

Because Node applications can legitimately spawn child processes via child_process, the normal behaviour of the environment should be observed for a while before the rule is enabled, and exceptions defined accordingly.

KQL for Microsoft Defender

In environments using Defender for Endpoint, the same logic can be hunted retrospectively with this query:

DeviceProcessEvents
| where Timestamp > datetime(2026-10-20)
| where InitiatingProcessFileName in~ ("node", "node.exe")
| where FileName in~ ("sh", "bash", "dash", "python3", "perl",
    "curl", "wget", "nc", "cmd.exe", "powershell.exe", "certutil.exe")
| project Timestamp, DeviceName, AccountName,
    InitiatingProcessFileName, FileName, ProcessCommandLine

Quick check on the server

On servers that have no EDR or do not send logs to a SIEM, the following commands do the job for a first check. Paths may vary by distribution and installation:

# Crashed Node processes (kernel messages)
journalctl -k --since "2026-10-20" \
    | grep -Ei "node.*(segfault|general protection)"

# Script files changed in the app directory in the last 3 days
find /srv/app -name "*.js" -mtime -3 -ls

# Source IPs of POST/PUT requests that returned 500
awk '$6 ~ /POST|PUT/ && $9 == 500 {print $1}' /var/log/nginx/access.log \
    | sort | uniq -c | sort -rn | head -20

If command lines show expressions such as /dev/tcp/, mkfifo or base64 -d, or reconnaissance commands such as whoami, id or uname -a run as the web user, treat it directly as an incident.

When an alert fires

  1. Isolate the server from the network, but do not shut it down. Memory and running processes have evidential value.

  2. Preserve the web server, Node process manager and system logs, together with a memory image if possible.

  3. Inspect new files in the app directory, new cron jobs, systemd services, SSH keys and user accounts.

  4. Extract the external addresses the server connected to, and search the SIEM for communication between those addresses and other assets.

  5. Rebuild the system from a clean image with a patched Node version. Simply patching a compromised server is not enough.

Finally, do not overlook the possibility of memory disclosure. Because this flaw can leak data without leaving any trace of code execution, on internet-facing systems where the patch was applied late it would be a sensible precaution to rotate database passwords, API keys, application secrets and, if necessary, TLS certificates.

Umut Yıldız
Cyber Security Engineer

Did you find this useful?

Be the first to receive our threat newsletters and MDR Insights reports.

Our team certifications

Experts accredited by SANS, Offensive Security, EC-Council, CompTIA, ISACA, CREST, and INE.

SANS GPEN
SANS GWAPT
SANS GICSP
SANS GRTP
SANS GCIH
SANS GSEC
Offensive Security OSCP
Offensive Security OSWP
EC-Council CEH
CompTIA Security+
ISACA CISM
ISACA CISA
CREST CRT
INE eWPTX
Fortinet FCP Secure Networking
Fortinet FCP Cloud Security
Fortinet FCP Security Operations
Fortinet FCSS Secure Networking
Fortinet FCSS SASE
Fortinet FCSS Cloud Security
Fortinet FCSS Security Operations
IBM QRadar Admin
SANS GPEN
SANS GWAPT
SANS GICSP
SANS GRTP
SANS GCIH
SANS GSEC
Offensive Security OSCP
Offensive Security OSWP
EC-Council CEH
CompTIA Security+
ISACA CISM
ISACA CISA
CREST CRT
INE eWPTX
Fortinet FCP Secure Networking
Fortinet FCP Cloud Security
Fortinet FCP Security Operations
Fortinet FCSS Secure Networking
Fortinet FCSS SASE
Fortinet FCSS Cloud Security
Fortinet FCSS Security Operations
IBM QRadar Admin

Cookie usage

We only use essential session and language preference cookies; no third-party tracking cookies. For details, see our Cookie Policy and KVKK Privacy Notice.