DEV Community

Cover image for OpenAI's Codex Found a Node.js Brotli RCE. Imperva Ran Code on an Official Build
Gabriel Anhaia
Gabriel Anhaia

Posted on AI-assisted

OpenAI's Codex Found a Node.js Brotli RCE. Imperva Ran Code on an Official Build


Eight bytes. That is the size of the internal buffer the Brotli decoder inside Node.js kept writing past, according to the Node.js security advisory of October 21. Feed it the wrong compressed stream and, in the advisory's words, an attacker "can corrupt process memory, which can lead to a crash, disclosure of process memory, or remote code execution."

The bug is CVE-2026-111497. The advisory credits researchers from OpenAI Codex Security with reporting it. On October 23, OpenAI described it on X as "a vulnerability that allows for unauthenticated remote code execution in Brotli." The same day, The Hacker News reported that Imperva had run code through it on the official Node.js 24.22.0 image for Linux arm64, with no debugger and no custom build.

If you run Node 22, 24 or 26 anywhere, the fix is an upgrade. If you run Node 20, no fix is coming.

What broke in the decoder

Brotli is the compression format behind Content-Encoding: br. Node ships its own copy of Google's decoder and exposes it through node:zlib.

The advisory gives the root cause in one sentence: "An invalid block switch was reported as a temporary need for more input instead of a permanent format error, so the streaming decoder kept appending input bytes to an eight-byte internal buffer." The decoder hit an impossible state, decided it was waiting for more data, and kept writing.

Node.js rates the bug high, not critical (advisory). Some outlets, such as Crypto Briefing, have printed a CVSS score of 9.2. The Hacker News notes the flaw "is rated high rather than critical." Go by the vendor's rating.

The upstream fix is older than the CVE. The change first landed in Brotli's source in February 2026, per The Hacker News, and the Brotli 1.2.1 changelog, dated October 21, lists the CVE under SECURITY. The change was labelled a security fix only in the October releases.

A compressed data parcel bursting open and spilling bytes over the edge of a small eight-slot tray
Eight slots, more bytes: the decoder kept appending input to a buffer that had no room for it.

Where the bytes get in

You don't have to call zlib yourself to be exposed. Imperva's analysis names two paths.

Responses you fetch. Node's built-in fetch() decoding a compressed response from another server reached the vulnerable decoder. A service that calls a third-party API, follows a webhook URL or fetches a URL a user typed is on this path when the server it fetches from can be controlled by an attacker.

Request bodies you parse. Express 5's body parsers, express.json(), express.urlencoded(), express.text() and express.raw(), decode Brotli request bodies alongside gzip and deflate (The Hacker News). Imperva reproduced execution on Express 5.0.0 to 5.3.0 and body-parser 2.0.0 to 2.3.0. A plain node:http server does not decode a Brotli body unless middleware or your own code does it.

Keep the result in proportion. Imperva says "execution took multiple attempts in our setup," and the team tested only arm64 on the 24.x line (The Hacker News). An earlier public proof of concept reached code execution only on a deliberately weakened build; on a standard build its author got a crash and a limited memory read (The Hacker News). No exploitation in the wild had been reported as of October 22, per the same article. A crash is still a denial of service, and the arm64 result shows code execution on a stock build is reachable.

Fixed versions, and the line that never gets one

The October 21 releases fix it in:

Line First fixed version
22.x (LTS) 22.23.4
24.x (LTS) 24.22.1
26.x (Current) 26.12.1
20.x and older none, end of life

The advisory is blunt about the last row: "End-of-Life versions are always affected when a security release occurs." It lists no workaround. Upgrading is the fix.

The same release patches nine other CVEs, three more of them high: two TLS certificate hostname bypasses and a dns.resolve truncation on NUL characters (advisory). One upgrade covers all of them.

Check every Node you actually run

Your laptop is the easy one. The Node binaries baked into container images, CI runners and VMs are the ones a dependency audit misses. Imperva says to check the Node version inside each deployed image or host, because "a package inventory can miss an older runtime bundled in a container" (Imperva).

Save this as check-node.mjs and run it with node check-node.mjs, or inside an image with docker run --rm -v "$PWD":/w your-image node /w/check-node.mjs:

// check-node.mjs: exit 1 if this Node lacks
// the CVE-2026-111497 fix
const fixed = { 22: [23, 4], 24: [22, 1], 26: [12, 1] };
const [maj, min, pat] = process.versions.node
  .split('.')
  .map(Number);
const f = fixed[maj];
const ok = Boolean(f) &&
  (min > f[0] || (min === f[0] && pat >= f[1]));
console.log(process.versions.node,
  ok ? 'patched' : 'NOT patched');
process.exit(ok ? 0 : 1);
Enter fullscreen mode Exit fullscreen mode

Any major not in the table, including 20, fails on purpose. A major newer than 26 will fail too, so extend the table when one ships. Run it as a CI step and the next advisory becomes a one-line table edit.

Then find the places your own code decodes Brotli directly:

git grep -nE "BrotliDecompress|brotliDecompress" \
  -- '*.js' '*.ts' '*.mjs' '*.cjs'
Enter fullscreen mode Exit fullscreen mode

For the version floor, engines in package.json takes a semver range, so you can join one floor per supported line with ||:

{
  "engines": {
    "node": "^22.23.4 || ^24.22.1 || >=26.12.1"
  }
}
Enter fullscreen mode Exit fullscreen mode

npm only warns on an engines mismatch by default, so treat it as documentation, not a gate. Pin your Dockerfile's base image to a fixed release and rebuild. A running container keeps the Node it was built with.

A stack of shipping containers, each marked with a small hexagon badge, with one container lifted under a large magnifying glass
The Node that matters is the one inside the image you deployed, not the one on your laptop.

If you're behind a platform WAF

Vercel deployed WAF rules on October 21 that block the inbound attack for every project, and it manages patch versions for its 22.x and 24.x runtimes. Its bulletin also says what that does not cover: the WAF "does not cover responses your functions fetch from other servers," and it "does not replace patching." Projects still on Node 20 have to move to 22 or 24 and redeploy.

Imperva has added WAF rules as well, and says a Node.js update is still required for full remediation. A WAF filters what comes in. A fetch() call pulls Brotli in from servers you don't control, and that response never passes an inbound rule.

Add check-node.mjs to the build of every image tag you still deploy, then see which ones fail. Which of your images would you have forgotten?


If this was useful

The hard part of this fix is knowing where your runtimes live: base images, CI runners, the platform's managed Node, the one VM nobody has touched in a year. Ship It covers that stretch between localhost and production for a small team, from hosting and deploys to CI/CD and the monitoring that tells you what you're actually running.

I also build Hermes IDE, so weigh this with that in mind: it's source-available (BSL 1.1), runs on macOS, Windows and Linux, and gives each task its own branch and worktree so Claude Code and Codex can work side by side. It's on GitHub.

Ship It — the pragmatic startup tech stack

Top comments (0)