
Node.js Brotli RCE CVE-2026-111497: Check fetch & Express 5
Skilldham
Engineering deep-dives for developers who want real understanding.
Last updated: October 2026
TL;DR
The Node.js Brotli RCE (CVE-2026-111497) is fixed in Node 22.23.4, 24.22.1 and 26.12.1. Upgrade today.
You are exposed if anything in your app decodes Brotli data an attacker controls. That includes:
Built-in fetch(), axios and node-fetch reading a response from a URL you do not control
Express 5 parsers reading a request body sent with Content-Encoding: br
Node 20 and older, which get no fix at all
The Node.js security release dropped on October 21, 2026. Ten CVEs across Node 22, 24 and 26.
One of them is a Node.js Brotli RCE rated High.
You skim the table. Brotli? Your app does not use Brotli.
You never called zlib.brotliDecompress in your life.
So you close the tab.
Here is the problem. Your app probably does decode Brotli.
It does it inside fetch(). It does it inside axios. If you moved to Express 5, it does it on JSON request bodies too.
None of that needs a single line of Brotli code from you. I ran every one of these paths in a test project to prove it. Below is the exact check, the fix, and the stopgap if you cannot upgrade tonight.
What the Node.js Brotli RCE Actually Is
The bug in plain words
Node.js ships its own copy of Google's Brotli decoder. It lives in deps/brotli and you reach it through node:zlib. A malformed Brotli stream makes the decoder write past the end of its buffer.
That is an out-of-bounds write. In the worst case, it means code execution as your Node process. At minimum, it can crash the process or leak memory.
Reports disagree on whether proof-of-concept code is public. Node.js and Vercel report no exploitation in the wild so far. Do not wait for that to change.
Fixed versions for every release line
The fix shipped on October 21, 2026. It covers all three supported lines, per the official Node.js October 2026 security release.
Release lineVulnerableFixed inNode.js 26.x26.12.0 and below26.12.1Node.js 24.x24.22.0 and below24.22.1Node.js 22.x22.23.3 and below22.23.4Node.js 25.xAll versionsNo fix - end of lifeNode.js 20.x and olderAll versionsNo fix - end of life
Node 20 users get nothing
Node 20 is end of life. It will not receive this patch. The same goes for Node 25.
My own MacBook was still on Node 20.20.0 when I ran these tests. That machine has no patch coming.
Does your Dockerfile still say FROM node:20? Then you run a known RCE with no patch coming.
The only fix is a major version jump to 22 or 24. Plan that today, not next sprint.
Am I Affected? The Reachability Check
The four questions that decide it
A vulnerable Node version alone is not the whole story. The bug needs attacker-controlled Brotli bytes to reach the decoder. Answer these four questions.
Does your server call fetch(), axios or node-fetch on a URL a user can influence?
Do you run Express 5 or body-parser 2.x on a public endpoint?
Does your code call zlib.brotliDecompress or createBrotliDecompress on uploads or external data?
Is your Node version below the fixed release for its line?
A "yes" to question 4 plus any other "yes" means you are exposed. Patch first, then read the rest.

The decision table
What your app doesDecodes Brotli by default?How to turn it offBuilt-in fetch() (undici) reads a responseYes, alwaysNo option exists. Upgrade Node.axios 1.x reads a responseYesdecompress: falsenode-fetch 3.x reads a responseYescompress: falseExpress 5 express.json() and other parsersYes, for request bodiesinflate: false or reject brExpress 4 parsers (body-parser 1.x)No - returns 415Not neededBrowser JavaScriptUses the browser's decoderNot affected
Inbound vs outbound risk
There are two directions to think about. Inbound means an attacker sends your API a Brotli body. Outbound means your server fetches a URL that returns a Brotli response.
Inbound is the scary one for Express 5 APIs. Anyone on the internet can POST to your endpoint. Outbound matters for webhooks, link previews, URL importers and scrapers.
Even "trusted" upstreams count. A compromised third-party API can send a malicious Brotli stream back to you.
fetch() Decodes Brotli Even When You Did Not Ask
The test I ran
I set up a tiny server that always answers with Content-Encoding: br. It ignores what the client asks for.
Then I called it from my MacBook on Node 20.20.0. I used every common HTTP client.
javascript
// Wrong: assuming Accept-Encoding: identity protects you
const res = await fetch('https://user-supplied-url.example', {
headers: { 'accept-encoding': 'identity' },
});
// This still prints the decoded JSON.
// fetch() decoded the Brotli body anyway.
console.log(await res.text());The result surprised me. I told the server I only accept identity.
The server sent Brotli anyway. And fetch() decoded it without complaint.
Why Accept-Encoding does not save you
Accept-Encoding is a request. It is not a contract. A hostile server can ignore it.
Node's fetch() looks at the response header, not your request header. If the response says br, undici decodes it.
No option stops that. Upgrading Node is the only real fix for fetch().
axios and node-fetch have an off switch
axios and node-fetch both decoded the Brotli body by default in my test. But both let you turn that off for risky calls.
javascript
// Correct: stopgap for clients that fetch untrusted URLs
import axios from 'axios';
import nodeFetch from 'node-fetch';
const url = 'https://user-supplied-url.example';
// axios: get the raw bytes, skip the Brotli decoder
const axiosRes = await axios.get(url, {
decompress: false,
responseType: 'arraybuffer',
});
// node-fetch: same idea
const nodeFetchRes = await nodeFetch(url, { compress: false });
const raw = Buffer.from(await nodeFetchRes.arrayBuffer());
console.log(axiosRes.data.byteLength, raw.length);With these flags on, both clients returned 21 raw bytes instead of decoded JSON. The decoder never ran. Only do this on calls to untrusted URLs, because you now handle compressed bytes yourself.
Express 5 Turned Brotli Bodies On by Default
Express 4 vs Express 5
This is the part most posts skip. Express 4 ships body-parser 1.x. It rejects a Brotli request body with a 415 error.
Express 5 ships body-parser 2.x. It decodes Brotli request bodies by default. If you upgraded to Express 5 this year, you opened a new door without knowing it.
I tested both. Express 4.x answered 415 unsupported content encoding "br". Express 5.3.0 with body-parser 2.4.0 answered 200 and parsed the body.
Still mid-migration? The Express 5 missing parameter name fix covers the routing side.
Why inflate: false is a blunt fix
The quick stopgap is the inflate option. It tells the parser to reject every compressed body.
javascript
// Wrong: this blocks Brotli, but it also breaks gzip clients
import express from 'express';
const app = express();
app.use(express.json({ inflate: false }));In my test, this returned 415 for Brotli. It also returned 415 for gzip. If a mobile app or partner sends gzip bodies, you just broke them.
A better stopgap: reject only Brotli
You can block just the br encoding and keep gzip working. Put this middleware before your body parsers.
javascript
// Correct: reject Brotli bodies, keep gzip and deflate working
import express from 'express';
const app = express();
function rejectBrotli(req, res, next) {
const header = req.headers['content-encoding'] || '';
const encodings = header.toLowerCase().split(',').map((s) => s.trim());
if (encodings.includes('br')) {
return res.status(415).json({ error: 'Brotli request bodies are not accepted' });
}
next();
}
app.use(rejectBrotli);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.post('/api/orders', (req, res) => {
res.json({ received: req.body });
});
app.listen(3000);In my test, Brotli bodies got a 415. Gzip bodies still parsed and returned 200. The check runs before any decoder touches the bytes.
This is a stopgap. It does not cover outbound fetch() calls. Upgrade Node anyway.
Find Every Brotli Code Path in Your Repo
Check your dependency tree
Start with the packages that decode Brotli for you. Each one is a door for the Node.js Brotli RCE.
bash
# List the Brotli-capable HTTP packages in this project
npm ls express body-parser axios node-fetchIf you see express@5 or body-parser@2, your inbound path is open. If you see axios or node-fetch, check what URLs they call.
Grep node_modules for the decoder
Your own code is only half the story. A dependency can call the Brotli decoder without you knowing.
bash
# Find every installed package that calls the Brotli decoder
grep -rlE "createBrotliDecompress|brotliDecompress" node_modules --include=*.js \
| awk -F/ '{print ($2 ~ /^@/ ? $2"/"$3 : $2)}' \
| sort -uOn my test project, this printed axios, body-parser and node-fetch. Built-in fetch() does not show up, because undici lives inside Node itself. On pnpm projects, run pnpm why on each package instead.
Grep your own code for fetch() on user input
Now look for outbound calls that take a URL from users. These are the risky ones.
bash
# Find fetch, axios and zlib calls in your source
grep -rnE "fetch\(|axios\.(get|post|request)|brotliDecompress" src app lib \
--include=*.js --include=*.ts --include=*.mjs 2>/dev/nullLook for URLs built from req.body, req.query or webhook payloads. Those are your outbound exposure points. Use the same habit as the npm supply chain attack lockfile check.
Verify the Node Version That Actually Runs
Your laptop is not your server
Running node -v on your laptop proves nothing about production. Check the runtime where the code actually runs.
bash
# Local and CI runners
node -v
# A built Docker image (skip the image's own entrypoint)
docker run --rm --entrypoint node myapp:prod -v
# A running container
docker exec my-container node -v
# A Kubernetes pod
kubectl exec deploy/my-api -- node -vThe --entrypoint flag matters. Many images start the app directly. Without the flag, node -v may never run.
Floating Docker tags lag behind
A tag like node:22-alpine does not update your image. It only updates the next build. Your running image keeps the old Node until you rebuild.
Docker's build cache makes this worse. If the base layer is cached, the rebuild can reuse the old Node. Use docker build --pull to force a fresh base image.
The safer fix is to pin the exact patched version.
dockerfile
# Correct: pin the exact patched Node version
FROM node:22.23.4-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]Now the version is in your diff. A reviewer can see it.
Native modules broke on rebuild? See the Node 24 native module Docker build fix.
Vercel and other managed runtimes
Vercel deployed WAF rules on October 21 to block inbound attacks. Its Node.js security bulletin has the details.
That protects request bodies sent to your Vercel projects. It does not cover responses your functions fetch from other servers.
Vercel manages patch versions for Node 22.x and 24.x, and says the rollout is in progress. Projects on Node 20.x must switch to 22.x or 24.x and redeploy. Self-hosted Next.js and external backends need a manual upgrade.
Running Next.js too? The Next.js October 2026 security release covers the framework flaws patched a week earlier. You likely need both upgrades.
Fail CI on a vulnerable runtime
Add a guard script to CI and to your container start-up. It fails loudly on an unpatched runtime.
javascript
// check-node-brotli.mjs
// Exits with code 1 if this Node.js runtime lacks the CVE-2026-111497 fix.
const FIXED = { 22: [23, 4], 24: [22, 1], 26: [12, 1] };
export function isPatched(version) {
const [major, minor, patch] = version.replace(/^v/, '').split('.').map(Number);
const fixed = FIXED[major];
if (!fixed) return false;
const [fixedMinor, fixedPatch] = fixed;
return minor > fixedMinor || (minor === fixedMinor && patch >= fixedPatch);
}
if (import.meta.url === `file://${process.argv[1]}`) {
const version = process.argv[2] || process.version;
if (isPatched(version)) {
console.log(`Node ${version} includes the CVE-2026-111497 fix.`);
} else {
console.error(`Node ${version} is NOT patched for CVE-2026-111497.`);
console.error('Upgrade to 22.23.4, 24.22.1 or 26.12.1 (or later on the same line).');
process.exit(1);
}
}Run it with node check-node-brotli.mjs. I tested it against twelve versions. It passes 22.23.4, 24.22.1 and 26.12.1, and fails 22.23.3, 24.22.0, 20.x and 25.x.
Do not check process.versions.brotli instead. The fix was a cherry-pick, so that value may not change.
The Other High-Severity CVEs in This Release
Two TLS hostname bypasses
CVE-2026-105483 lets a certificate check pass when an IP is written in fullwidth digits. It affects Node 24.x and 26.x.
CVE-2026-106069 is a second hostname bypass. It triggers when a host loosely resolves to an IP address.
It hits all three lines. Both matter if you rely on TLS checks to block SSRF.
The dns.resolve NUL truncation
CVE-2026-105482 is the fourth High. dns.resolve truncates names that contain a NUL character. An allow-list check can see one name while DNS resolves another.
The same upgrade fixes all four. You do not need a separate patch for each one.
Key Takeaways
The Node.js Brotli RCE (CVE-2026-111497) is fixed in Node 22.23.4, 24.22.1 and 26.12.1.
Node 20 and Node 25 get no fix. Move to 22 or 24 now.
Built-in fetch() decodes Brotli responses even if you send Accept-Encoding: identity.
Express 5 decodes Brotli request bodies by default. Express 4 does not.
Use a reject-Brotli middleware as a stopgap. inflate: false also breaks gzip clients.
Check the runtime that serves traffic: Docker images, pods and Vercel settings, not your laptop.
Pin FROM node:22.23.4-alpine and rebuild with --pull. Floating tags only help on the next build.
FAQ
Does the Node.js Brotli RCE affect apps that never use Brotli?
Probably yes. Built-in fetch(), axios, node-fetch and Express 5 parsers all decode Brotli without any code from you. On an unpatched Node, any of them can expose you.
Which Node.js versions fix CVE-2026-111497?
Node 22.23.4, 24.22.1 and 26.12.1 fix it. Any later release on the same line also includes the fix. All three shipped on October 21, 2026.
Will Node.js 20 get a patch for the Brotli RCE?
No. Node 20 is end of life and will not receive this fix. Node 25 is also end of life, so upgrade to Node 22 or 24.
Does setting Accept-Encoding to identity protect fetch()?
No. In my test, a server sent Brotli anyway and fetch() decoded it. It reads the response header, so the only fix is to upgrade Node.
Is Express 4 affected by CVE-2026-111497?
Express 4 parsers do not decode Brotli request bodies. They return a 415 error instead. Your Express 4 app can still be exposed through fetch() or axios on an unpatched Node.
Does inflate: false fully protect an Express 5 API?
It blocks Brotli request bodies, but it also rejects gzip and deflate bodies. It does nothing for outbound fetch() calls. Use a middleware that rejects only br, then upgrade Node.
Is my Vercel deployment already safe?
Partly. Vercel WAF rules from October 21 block inbound attacks, but not responses your functions fetch. Node 20 projects must move to 22.x or 24.x and redeploy.
How do I check the Node version inside a Docker image?
Run docker run --rm --entrypoint node your-image -v. The entrypoint flag skips the image's own start command. For a running container, use docker exec your-container node -v.
Fix It Today, Then Lock It In
The Node.js Brotli RCE hides in code you never wrote: fetch(), axios and Express 5 parsers. That is why a version table is not enough. You need to know which paths touch Brotli and which Node actually runs in production.
Upgrade to 22.23.4, 24.22.1 or 26.12.1. Add the reject-Brotli middleware if you cannot deploy tonight. Then add the CI check so a stale image never ships again.
The Next.js July 2026 security release checklist uses the same method. Subscribe to the SkillDham newsletter. You get the next Node.js security fix the day it lands.