I just wrote about PHP2Shell and PHP’s request-lifecycle bugs. Now Node.js has its own runtime security update to deal with. Different engine, different failure mode, same uncomfortable reminder: safe-looking application code still depends on native code underneath it.

On 21 October 2026, Node.js released fixes for 10 CVEs. The one I want to spend time on is CVE-2026-111497, an out-of-bounds write in the bundled Brotli decoder that permits remote code execution.

Before we get carried away: these are not 10 Critical vulnerabilities. The official breakdown is four High, three Medium and three Low. The Brotli RCE is rated High. RCE describes an impact, not an automatic severity label.1

Research cut-off: 22 October 2026. This write-up combines the official Node.js announcement, its public patch, upstream Brotli changes and Vercel’s advisory. I tested the benign local demonstration below on Node.js 26.12.1. I have not independently reproduced memory disclosure or RCE, and I am not claiming discovery credit.

Quick answer: what is CVE-2026-111497?

CVE-2026-111497 is a High-severity memory-corruption vulnerability in Node.js’s Brotli decoder. An application is exposed when it or a dependency decompresses attacker-controlled Brotli data, including fetched HTTP responses. Upgrade to Node.js 22.23.4, 24.22.1 or 26.12.1, or a later patched release on the same supported branch.12

QuestionAnswer
Vulnerable componentNative Brotli decoder bundled with Node.js
Official severityHigh
Reported impactProcess-memory disclosure and command execution
Exposure conditionAttacker-controlled compressed data reaches Brotli decompression
Affected supported branches22.x, 24.x and 26.x before their respective fixes
First fixed releases22.23.4, 24.22.1 and 26.12.1
Does an inbound WAF solve it?No. Fetched responses are another exposure path
Is updating npm dependencies enough?Not if the vulnerable decoder is still in the Node.js runtime

Vercel reports that researchers demonstrated memory disclosure and command execution on unpatched versions of all three supported branches. Execution occurs with the permissions of the Node.js process, not automatically as root.2

For Malaysian development teams running Next.js, APIs, import workers or containerized Node services, the ownership question is practical: who patches the actual runtime, and who redeploys every process using it?

Brotli is not just something your CDN does

Brotli is a compression format. On the web, a response can advertise it with:

Content-Encoding: br

A consumer decompresses those bytes before treating them as HTML, JSON, an image or another application format.

Node.js exposes Brotli decoding through node:zlib, including brotliDecompress(), brotliDecompressSync() and createBrotliDecompress(). HTTP clients and framework features can call that functionality for you.34

That is the part I would investigate first in a VAPT assessment. A repository does not need an obvious brotliDecompress() call for compressed attacker-controlled data to reach the runtime.

Two useful threat models:

Inbound: a request-body parser or another component decompresses a Brotli body supplied to your service. Node’s basic HTTP server does not automatically turn every request body into decoded content; the actual middleware and data flow matter.

Outbound: your service fetches a response from an attacker-controlled or compromised server, and the HTTP client decompresses it. URL previews, remote imports and image-processing pipelines deserve review. Vercel specifically notes that self-hosted Next.js image optimization does not block Brotli responses.2

Serving precompressed static assets is not, by itself, evidence that your Node process invokes the vulnerable decoder. Follow the bytes. Do not infer exploitability from the word “Brotli” in a configuration file.

How the bug happened: the public patch tells a useful story

The Node.js 24.22.1 release identifies the Brotli fix as a cherry-pick of upstream commit 0ed4268a4401abcb4472096340477cf4af7d1a2b. Its title is “Detect and report invalid stream earlier.” The Node backport explicitly references CVE-2026-111497.567

The relevant changes are in:

  • deps/brotli/c/dec/decode.c
  • deps/brotli/c/include/brotli/decode.h

This is C decoder code shipped with the runtime, not an ordinary JavaScript package in your application’s lockfile.

1. Invalid format and insufficient input need different answers

The patch changes DecodeBlockTypeAndLength() from returning a boolean to returning BrotliDecoderErrorCode.

Previously, an invalid block-switch condition and cases where more input was needed could both return BROTLI_FALSE. After the patch, those outcomes become distinguishable.5

This is a shortened excerpt of the actual change:

-static BROTLI_INLINE BROTLI_BOOL DecodeBlockTypeAndLength(
+static BROTLI_INLINE BrotliDecoderErrorCode DecodeBlockTypeAndLength(
     int safe, BrotliDecoderState* s, int tree_type) {
   ...
   if (max_block_type <= 1) {
-    return BROTLI_FALSE;
+    return BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH;
   }

Other return sites now explicitly distinguish BROTLI_DECODER_NEEDS_MORE_INPUT from BROTLI_DECODER_SUCCESS.

That matters for a streaming parser. “Wait for another chunk” and “this stream is invalid” are not the same state transition.

2. The fast path cannot ignore format errors

The patch documents an assumption in the existing BROTLI_SAFE macro: a false result should mean insufficient input. In the fast path, where sufficient input is assumed, the result could be ignored.5

But having enough bytes does not mean those bytes describe a valid stream.

The fix introduces BROTLI_SAFE_WITH_STATUS. It captures the result in either decoding path and exits processing on anything other than success. Literal, command and distance block-switch call sites use the new macro.

One example:

-    BROTLI_SAFE(DecodeCommandBlockSwitch(s));
+    BROTLI_SAFE_WITH_STATUS(DecodeCommandBlockSwitch(s));

The patch also propagates statuses through the block-switch helper functions and adds the dedicated BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH error.

3. What we can conclude, and what we cannot

Directly visible in the patch: invalid block-switch handling needed explicit status separation and propagation, including in the fast decoding path.

Confirmed by the advisories: malformed Brotli input can cause an out-of-bounds write; Vercel reports demonstrated memory disclosure and command execution.12

Not independently established here: the exact crafted bitstream, which allocation a particular exploit corrupts, how it obtains a reliable execution primitive, or portability across builds and platforms.

The source diff explains an important failure mechanism. It is not a substitute for an instrumented exploit trace. I would rather show that boundary than invent a heap layout because it sounds impressive.

Why the outbound path is easy to miss

The Node.js 24.22.1 source includes an Undici fetch implementation that examines the response’s Content-Encoding. When it encounters br, it creates a zlib.createBrotliDecompress() stream.4

The conceptual path is:

Application fetches an external resource
  -> remote server returns Content-Encoding: br
  -> HTTP client selects the Brotli decoder
  -> attacker-controlled bytes reach native parsing
  -> malformed input can trigger the vulnerable decoder

That does not mean every fetch() call is exploitable. The attacker needs a relevant way to control the response that gets decoded, and the runtime must be affected.

It does mean an inbound firewall is not the whole answer. A WAF inspecting requests to your application does not automatically inspect the bodies your application fetches from somewhere else.

Also, Accept-Encoding: identity is a request to the server, not a security guarantee that hostile responses will never be compressed. Any temporary “no Brotli” control must actually prevent decoder invocation at the correct layer. Do not assume a header change, or a check after fetching begins, proves that.

PoC status: a working exploit is not the same as a demo

As of this write-up, Vercel reports demonstrated exploitation. I have not verified a public working RCE PoC. That is not a claim that no public PoC exists.2

There are several different levels of evidence:

EvidenceWhat it establishes
Affected runtime version without a vendor fixComponent exposure
Application trace showing untrusted input entering Brotli decodingReachability
Matching memory-safety failure in an isolated instrumented buildReproduction of a native defect
Controlled execution from that defect in an authorized labExploitability of that tested environment

A crash is not automatically RCE. Successful decompression is not a vulnerability test. A random repository named after the CVE is not automatically trustworthy.

A runnable, local reachability demonstration

The following code demonstrates the often-overlooked outbound response path. It starts a loopback-only HTTP server, returns a valid Brotli response, and shows Node’s built-in fetch() returning decoded text.

This is not an RCE PoC or a patch detector. It uses valid compressed data, does not attempt memory corruption, and should work on both vulnerable and patched runtimes. Run it on a supported, patched Node version.

Save as brotli-reachability.mjs:

import assert from 'node:assert/strict';
import { once } from 'node:events';
import { createServer } from 'node:http';
import { brotliCompressSync } from 'node:zlib';

const message = 'Local Brotli reachability demo, not an exploit.';
const compressed = brotliCompressSync(Buffer.from(message));
const server = createServer((_request, response) => {
  response.writeHead(200, {
    'Content-Type': 'text/plain',
    'Content-Encoding': 'br',
    'Content-Length': compressed.length,
    Connection: 'close',
  });
  response.end(compressed);
});

server.listen(0, '127.0.0.1');
await once(server, 'listening');

try {
  const { port } = server.address();
  const response = await fetch(`http://127.0.0.1:${port}/`, {
    signal: AbortSignal.timeout(5000),
  });
  const decoded = await response.text();
  assert.equal(decoded, message);
  console.log(JSON.stringify({
    node: process.version,
    brotli: process.versions.brotli,
    contentEncoding: response.headers.get('content-encoding'),
    decoded,
  }, null, 2));
} finally {
  await new Promise((resolve, reject) => {
    server.close(error => error ? reject(error) : resolve());
    server.closeAllConnections();
  });
}

Run:

node brotli-reachability.mjs

The tested output on this environment was:

{
  "node": "v26.12.1",
  "brotli": "1.2.0",
  "contentEncoding": "br",
  "decoded": "Local Brotli reachability demo, not an exploit."
}

Notice the interesting inventory detail: this patched Node build still reports Brotli 1.2.0. The Node release includes a cherry-picked fix, so the dependency’s displayed version alone is not enough to decide whether the fix is present.5

If I were validating an actual malformed-input reproducer, I would use an authorized isolated environment, a known affected build and its patched counterpart, memory-safety instrumentation and no production data. This post does not supply or claim to have tested such a reproducer.

Why a High-severity RCE deserves urgent attention

The severity label should stay accurate. It should not become an excuse to put the update at the bottom of the backlog.

The operational concern is the process boundary. A Node worker may hold database credentials, API tokens, session material or cloud-service access. Code execution as that worker can be damaging without root access. Memory disclosure can also be serious without command execution.2

From a research perspective, the interesting boundary is:

Untrusted compressed bytes become input to a native parser before the application sees the decoded content it wanted to validate.

JavaScript schema checks, TypeScript types and a try/catch around a request do not repair native memory corruption. Nor does a clean application SAST report.

I am not assigning a numerical CVSS score here because the cited Node.js announcement gives the severity category, not a score. I also do not infer active exploitation in the wild from a report that researchers demonstrated an exploit.

The other nine Node.js CVEs, briefly

The following severities and affected supported branches come from the official release announcement. The “review focus” notes are my defensive interpretation, not claims of independently reproduced exploit chains.1

Three more High-severity issues

CVE-2026-105482: DNS name truncation at a NUL character. Affects 22.x, 24.x and 26.x. dns.resolve could truncate names containing a NUL character. The review concern is inconsistent interpretation: application logic evaluates one string while resolution processes a shorter one. Audit user-controlled hostname validation and resolution boundaries. Do not claim a complete SSRF bypass without showing the application’s full validation flow.

CVE-2026-105483: TLS hostname bypass with fullwidth IP digits. Affects 24.x and 26.x. The advisory describes certificate hostname verification bypass when an IP is written using fullwidth digits. Review normalization and certificate identity checks together. This is not a claim that TLS encryption itself is broken.

CVE-2026-106069: TLS hostname bypass for loosely interpreted IP addresses. Affects 22.x, 24.x and 26.x. Hostnames that loosely resolve to an IP address could bypass hostname verification. Treat address parsing and the identity authenticated by the certificate as one trust-boundary review, rather than assuming all layers normalize hosts identically.

Three Medium-severity issues

CVE-2026-105481: premature responses on keep-alive sockets. Affects 22.x, 24.x and 26.x. A reused connection could process a response written before the corresponding request was sent. The relevant concern is request/response association and connection state. Review pooled HTTP clients and avoid assuming a fresh logical request always means a fresh transport connection.

CVE-2026-106072: incorrect certificate validation for non-canonical IPv4 hostnames. Affects 22.x, 24.x and 26.x. DNS Subject Alternative Names could authenticate IP endpoints under the described non-canonical hostname conditions. This is another identity interpretation problem, separately tracked and rated Medium. Patch rather than trying to repair all address representations with an application regex.

CVE-2026-106074: random caches survive snapshots and single executable applications. Affects 22.x, 24.x and 26.x. Cached values for crypto.randomUUID() and randomInt() could be saved into user-land startup snapshots and SEAs, causing processes created from those artifacts to repeat cached values. This is not a claim that every ordinary Node process repeats every random value. Audit artifact creation and startup behavior, then rebuild relevant artifacts with the fixed runtime.

Three Low-severity issues

CVE-2026-106070: ZIP prefix bytes returned after failed validation. Affects 26.x. Node’s ZipFile could return attacker-controlled prefix bytes after ZIP header validation failed. Consumers should not treat data from a failed validation path as trusted. The announcement does not describe this as a general arbitrary-file-read or RCE primitive.

CVE-2026-106071: excessive filesystem permission from wildcard lookup. Affects 26.x. A wildcard radix lookup in the Permission Model could grant more filesystem access than intended. Review applications depending on those restrictions and test denied paths, not only allowed ones.

CVE-2026-106073: Permission Model bypass through fs.realpathSync. Affects 22.x, 24.x and 26.x. The synchronous realpath operation could bypass the model’s checks. Patch and retest permission-restricted workflows. Do not confuse a Node Permission Model defect with an automatic bypass of operating-system permissions.

Together with the Brotli issue, that makes four High, three Medium and three Low, with only Brotli explicitly identified as RCE in this announcement. The release also updates dependencies and certificates, so the 10 named CVEs are not a reason to cherry-pick just one fix.1

My VAPT workflow for this release

1. Establish the runtime actually executing the code

Start with authenticated inventory:

node --version
node -p 'JSON.stringify({
  executable: process.execPath,
  node: process.version,
  brotli: process.versions.brotli,
  undici: process.versions.undici
}, null, 2)'

These commands describe the Node executable invoked in that shell. They do not prove what a system service, container, function, process manager or background worker is running.

Collect the executable path, full package revision or image digest, deployment identity and patch evidence for each workload. A developer’s updated terminal is not evidence that production has restarted.

2. Trace decoding from both directions

Search application and dependency code for Brotli APIs, response decompression, body parsers, URL fetches, preview generation, remote image processing and file imports.

Classify each path:

  • Who controls the bytes or remote endpoint?
  • Which client or parser handles them?
  • Does it invoke Brotli automatically?
  • Is the runtime fixed?
  • What privileges and secrets does the process hold?

A source search is a starting point. It can miss generated code, indirect calls and automatic dependency behavior.

3. Report evidence at the right confidence level

An affected-runtime finding is valid without production exploitation. Add reachability evidence when available. Do not label a successful benign fetch as “RCE confirmed”.

For remediation closure, I want a patched runtime or documented vendor backport, replacement of old processes, representative workload checks and evidence covering every replica. Keep an exception open if a vendor-managed appliance or embedded runtime remains unresolved.

What SAST should look for

Application SAST cannot normally prove that a bundled C decoder is memory-safe. Software composition analysis and runtime inventory are essential, and npm audit alone is not a complete runtime audit.

For this release, my review targets would be:

  • Data flow: untrusted requests and remote response bodies reaching decompression.
  • Native parser error handling: invalid format versus incomplete input, ignored return values and inconsistent fast-path behavior.
  • Identity interpretation: DNS names, unusual IP forms and certificate matching.
  • Process initialization: random-state caches captured in snapshots.
  • Authorization consistency: synchronous APIs and wildcard permission matching.

The Brotli patch offers a particularly useful review question:

Does this optimized path prove the input is valid, or merely prove that enough bytes are available?

Those are different guarantees. A parser that mixes them can keep executing after it should have rejected the stream.

Remediation and verification

Upgrade the complete runtime

Supported branchFirst fixed version
Node.js 22.x22.23.4
Node.js 24.x24.22.1
Node.js 26.x26.12.1

Use those releases or later patched releases in the same supported branch. For vendor-maintained packages, verify the vendor’s backport evidence rather than relying only on the upstream version string.1

Node.js 20 is end-of-life and does not receive this fix according to Vercel’s guidance. Move to a supported line. Do not read the three supported branches in the announcement as a declaration that older runtimes are safe.21

Rebuild container images, redeploy functions and restart or replace services and workers. Rebuild snapshot and SEA artifacts where applicable. Update CI runners and local runtimes too, especially if they process untrusted external data.

Treat WAF rules as partial protection

Vercel’s 21 October advisory says it deployed WAF rules for the inbound attack vector and was rolling patched managed runtimes out. That is dated provider guidance, not a claim here that every rollout is already complete.2

Follow current provider instructions. Vercel says projects on managed 22.x or 24.x do not need to change their major-version setting, while projects still on 20.x need to select a supported version and redeploy.

External backends and self-hosted containers remain your responsibility. Inbound protection does not cover fetched responses.

If you cannot patch immediately, temporarily disable affected ingestion or fetching features where feasible, restrict reachable sources and remove unnecessary Brotli decoding. Verify those controls prevent invocation of the decoder. Size limits and timeouts are useful availability defenses, not proof that malformed compressed input cannot trigger this memory-safety bug.

Verify functionality and investigate suspicious behavior

After updating, check ordinary API requests, compressed responses, imports, image processing, TLS connections and permission-restricted jobs. Confirm that all replicas use the intended build.

My generic investigation leads would include unexpected native crashes, worker restarts, child processes, outbound connections and file changes around workloads that decode remote data. These are triage ideas, not CVE-specific indicators of compromise. Decoder errors alone are not proof of exploitation.

If compromise is suspected, preserve evidence and follow incident response procedures. Updating Node does not revoke stolen credentials or remove persistence.

Frequently asked questions

Are all 10 Node.js vulnerabilities Critical or RCE?

No. The official release lists four High, three Medium and three Low. CVE-2026-111497 is the one explicitly described as permitting remote code execution.1

Does CVE-2026-111497 affect every Node.js HTTP server?

Not automatically. Attacker-controlled Brotli data must reach a decoder through application code or a dependency. Incoming parsing and outbound response handling both need review.2

Can a normal fetch call reach the Brotli decoder?

Yes. The inspected Undici implementation creates a Brotli decoder for responses carrying Content-Encoding: br. The local demonstration above shows automatic decoding of valid data, not exploitation.4

Is a reported Brotli version of 1.2.0 proof that Node is unpatched?

No. The tested patched Node.js 26.12.1 build still reported 1.2.0, and the release uses a cherry-picked Brotli fix. Verify the Node build and vendor patch status, not that field alone.5

Will updating Next.js or running npm audit fix the runtime?

Not by itself. Update the Node runtime that actually executes the application. Continue dependency maintenance, but do not substitute it for this security release.

Is there a verified RCE payload in this article?

No. Vercel reports demonstrated exploitation, but I have not verified a public exploit implementation. The included runnable code is a benign reachability demonstration, clearly not an exploit or patch test.

The takeaway

The PHP story was about state surviving the wrong lifecycle boundary. This one is about a native parser treating failure states too loosely, including on an optimized path.

For defenders, the lesson is refreshingly practical: inspect the runtime, trace compressed data from both directions, and patch the processes that actually run.

And when someone says “the WAF already covers it”, ask what happens when the application is the HTTP client.

Sources and credits

Primary sources are linked at the relevant claims and collected below for further reading. Patch interpretation and assessment recommendations are my analysis. The featured illustration uses the authentic Node.js icon from Simple Icons in an original composed graphic.


  1. The Node.js Project. “Wednesday, October 21, 2026 Security Releases.” 21 October 2026. Official list of 10 CVEs, severity categories, affected supported branches, dependency updates and fixed releases. Security announcement. Accessed 22 October 2026. For CVE-2026-111497, the announcement credits Matteo Collina (mcollina) for reporting and Antoine du Hamel (aduh95) for fixing. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Vercel Security Team. “Node.js security advisory: Brotli decoder RCE (CVE-2026-111497).” 21 October 2026. Reported exploitation impact, inbound and outbound exposure, WAF scope, managed-runtime guidance and Node.js 20 migration. Full advisory. Accessed 22 October 2026. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  3. The Node.js Project. Zlib API documentation, Node.js v24.22.1. Brotli decompression interfaces. Versioned API reference. Accessed 22 October 2026. ↩︎

  4. Node.js / Undici contributors. Bundled Fetch implementation, Node.js v24.22.1, deps/undici/src/lib/web/fetch/index.js. Response content-coding handling invokes zlib.createBrotliDecompress() for br. Version-pinned source. Accessed 22 October 2026. ↩︎ ↩︎ ↩︎

  5. Evgenii Kliuchnikov, Node.js repository. “deps: brotli: cherry-pick 0ed4268a4401abcb44720963.” Node commit 49dff6f57aa72cbbe13e480cd41ff4e46837c8fc, included in the 21 October 2026 Node.js 24.22.1 release. Explicitly references CVE-2026-111497. Complete patch and commit metadata. Accessed 22 October 2026. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  6. Evgenii Kliuchnikov, Google Brotli repository. “Detect and report invalid stream earlier.” Upstream commit 0ed4268a4401abcb4472096340477cf4af7d1a2b, authored 27 February 2026. Original Brotli change. Accessed 22 October 2026. The upstream author date should not be mistaken for the Node.js security disclosure date. ↩︎

  7. Antoine du Hamel, The Node.js Project. “Node.js 24.22.1 (LTS).” 21 October 2026. Release notes and commit list linking the Brotli backport and other security changes. Release notes. Accessed 22 October 2026. ↩︎