# Node.js Fixed the Brotli Flaw on October 21. Our Hosted Functions Were Still on the Old Build Six Days Later

Node.js fixed the Brotli decoder flaw on October 21, 2026. Six days later our hosted functions still ran the old build. Who is exposed, how to check, and what we changed.

Author: J.A. Watte
Published: October 27, 2026
Source: https://jwatte.com/blog/brotli-cve-2026-111497-patch-check/

---

On October 21, 2026, Node.js shipped a security release that fixed ten problems at once. The one everyone noticed was CVE-2026-111497, a bug in the Brotli decoder that Node carries inside it. Hand that decoder a carefully broken compressed stream and it writes past the end of the memory it was given. At best the process crashes. At worst whoever built the stream gets to run their own code inside yours.

Over the next five days Python's Brotli packages, Bun, .NET, PowerShell, Visual Studio, Git for Windows and Ubuntu all shipped fixes, because they all carry a copy of the same decoder.

On October 27 I asked what sounded like a quick question: do any of our repos or sites still need this patch? I expected a short no. Our servers update every night, and nothing we write calls a Brotli function by name. The real answer was yes, and in a place none of our repos control. The serverless functions behind jwatte.com, including the ones that fetch pages for the free audit tools, were running Node 24.21.0 that morning. That is the release before the fix.

This post covers what the bug is, who is actually exposed, how we checked a few dozen sites in an afternoon, what we changed, and a checklist you can run on your own setup tonight.

## What broke

Brotli is a compression format. Most websites send their pages Brotli compressed because the files come out smaller than gzip, and every modern browser and HTTP client knows how to unpack them. The unpacking happens in a decoder, a small piece of C code that Google maintains and that nearly everyone copies into their own software.

Versions 1.0.3 through 1.2.0 of that decoder can write out of bounds. Google fixed it in 1.0.10, 1.1.1 and 1.2.1, all released on October 21.

Two details make this worse than an average library bug.

First, the decoder runs before your code ever looks at the data. Nobody has to click anything, and your program does not have to trust or parse the reply. If your server asks another server for a page and gets back a hostile Brotli stream, the damage happens while the response is being unpacked.

Second, in Node you do not have to ask for Brotli to get it. Node's built in `fetch()` comes from a library called undici. We read its source in the patched Node 24.22.1 tree. It asks for Brotli by default on HTTPS requests, and it decodes whatever encoding the response claims, Brotli included, even if your request said you only wanted gzip. So changing request headers does not protect you. Only the patched runtime does.

## Who is exposed and who is not

It comes down to one question: does your software unpack compressed data that somebody else chose?

You are exposed if something you run fetches addresses other people pick, or accepts compressed uploads from the public. That covers:

- link previews that fetch whatever URL a user pastes
- SEO, accessibility and security audit tools, including the free ones on this site
- RSS and news readers that pull from many feeds
- AI agents with a browse or fetch tool, and chat apps with web search
- relays, webhook forwarders and caching proxies
- APIs that accept request bodies sent with `Content-Encoding: br`

You are probably fine if:

- your site is static files served from a CDN with no server code
- your functions only call a few fixed services over HTTPS, like a payment provider or an email API, since an attacker would have to break those services first
- your code runs on Deno, which uses a decoder written in Rust instead of Google's C library; write ups so far say it is not affected

Here is what was fixed where, as of October 27:

| Software | Fixed in | Worth knowing |
| --- | --- | --- |
| Google brotli (C library) | 1.0.10, 1.1.1, 1.2.1 | Affected range is 1.0.3 to 1.2.0 |
| Node.js | 22.23.4, 24.22.1, 26.12.1 | Node 20 and older get no fix |
| Python `brotli` / `brotlicffi` | 1.2.1 / 1.2.1.0 | HTTP clients such as urllib3 use these whenever they are installed |
| Bun | 1.4.4 | Rebuild anything made with `bun build --compile` |
| .NET runtime | 8.0.33, 9.0.22, 10.0.14 | Republish self contained apps and container images |
| PowerShell | 7.4.22, 7.5.13, 7.6.9 | Built on the patched .NET SDKs |
| Visual Studio 2022 | 17.14.43 | |
| Git for Windows | 2.56.0.windows.4 | Its curl carries its own Brotli |
| Ubuntu | USN-8956-1 | Patches the shared system library only |
| Cloudflare Workers | patched in production | Local `wrangler dev` still bundled an older runtime |

That Ubuntu row deserves a second look. Updating the operating system patches the shared library. It does nothing for Node, Python wheels, Bun, Git for Windows or any program that brought its own copy, which is most of them. Our server installed Ubuntu's fixed library automatically at 06:51 UTC on October 27. That same afternoon the chat app container on the same machine still carried its own unpatched Brotli package, because containers ship their own libraries too. A fresh image fixed it; the host update never could.

## The version number lies

When you check whether Node is patched, it is tempting to ask Node which Brotli it has with `process.versions.brotli`. Do not trust the answer. The fix went into Node's copy without changing that version string, so a patched Node 24.22.1 still reports Brotli 1.2.0, exactly like the broken 24.21.0. Patched Node 22.23.4 reports 1.1.0, which sits inside the affected range on paper.

Check the Node version itself:

```
node -p process.version
```

You want v22.23.4, v24.22.1, v26.12.1 or anything later on the same line. Anything on Node 20 or older has to move to a supported line, because no fix is coming.

## The runtime you never installed

This is where we got caught. jwatte.com runs on Netlify, and its server code runs as Netlify Functions on top of AWS Lambda's managed Node runtimes. You pick the major version. Amazon picks the patch level and rolls it out on its own schedule. No file in your repo controls it, so reading our code could never answer the question.

We had to ask a running function. We deployed a tiny one to a throwaway site and to a draft deploy, never to production, that does nothing but report what it runs on:

```js
export default async () => new Response(JSON.stringify({
  node: process.version,
  brotli: process.versions.brotli,
  runtime: process.env.AWS_EXECUTION_ENV || null,
}), { headers: { 'content-type': 'application/json', 'cache-control': 'no-store' } });
export const config = { path: '/rt-probe' };
```

On October 27, 2026 it answered:

| Netlify functions runtime | Node it reported | Patched? |
| --- | --- | --- |
| `nodejs24.x` (Netlify's default for new sites since July) | v24.21.0 | No |
| `nodejs22.x` | v22.23.4 | Yes |

So the default Node 24 runtime was still one release behind six days after the fix, while the older Node 22 runtime already had it.

Netlify lets you pick the runtime line with one environment variable, `AWS_LAMBDA_JS_RUNTIME`, set in the site's environment settings. The docs say it cannot go in `netlify.toml`. Setting it changes nothing on its own, because functions keep the runtime they were deployed with, so for each exposed site we did four things:

1. Ran the site's function tests locally on Node 22.23.4, to make sure nothing depended on something only Node 24 has.
2. Set `AWS_LAMBDA_JS_RUNTIME` to `nodejs22.x` for the functions scope.
3. Made a draft deploy from the exact tree that was live and compared it with production: same function names, same schedules, every function now on `nodejs22.x`.
4. Promoted it to production and read the live deploy back to confirm the runtime on every function.

This is a stopgap, not a downgrade we plan to keep. Node 22 is a supported long term release line until April 2027, so it is a safe place to wait. When a probe shows the Node 24 runtime at 24.22.1 or later, we delete the variable and redeploy.

## How we checked a few dozen sites in one afternoon

We look after 47 sites across six Netlify teams: our own, some friends' and a few small business sites. Checking them one by one in a browser would have taken days. What made it fast is that every Netlify deploy record lists each function along with the Lambda runtime it runs on. One API call per site gave us the whole picture:

| What the site runs | Sites | What we did |
| --- | --- | --- |
| Static files only, or edge functions on Deno | 14 | Nothing to patch |
| Functions already on `nodejs22.x` | 3 | Already patched |
| Functions on `nodejs24.x` that fetch addresses other people choose | 2 | Moved to Node 22 and redeployed the same day |
| Functions on `nodejs24.x` that only call fixed services or their own site | 28 | Left alone until Amazon ships 24.22.1, then probe again |

The interesting work was sorting that last group, because a function that never fetches anything outside is not reachable by this bug. For each function we read every outbound request and sorted it into one of three buckets: no outbound requests at all, fixed services we trust (payments, email, storage, the site's own pages), or addresses a caller can pick. Two sites had functions in the third bucket. One was this site's page fetcher. The other was a news tool with an open fetch endpoint and a few dozen feed readers. Both moved to Node 22 the same day.

We did not redeploy the low risk sites. Several belong to friends, and a redeploy is never free: it is a chance to publish an old copy of a site by accident. On several of the sites we checked, the copy on GitHub was older than what was live, and redeploying from it would have quietly rolled back weeks of changes, in one case putting an old phone number back on a business site. If you redeploy anything in a hurry, prove first that the source you are about to deploy is the source that is live.

## Cloudflare, briefly

Our Cloudflare Workers include a relay that fetches any URL a trusted caller sends it, so it sits squarely in the exposed group. Cloudflare runs Workers on its own runtime, and a Cloudflare maintainer said on October 27 that production had been patched. Nothing on our side needed a redeploy.

Two things still mattered. The local development runtime that `wrangler dev` downloads lagged behind production, so until a fixed wrangler release lands, keep local Worker tests away from outside URLs. And Cloudflare published emergency WAF rules for this bug on October 25, set to log rather than block by default. If you put Cloudflare in front of an origin that accepts compressed uploads, look at those rules and decide on purpose.

## Keeping repos current without wrecking work in progress

The sweep turned up the slow kind of problem too, the kind that never makes headlines.

- **Security alerts were off.** Dependabot alerts were disabled on every one of our repos. Turning them on is one API call per repo and costs nothing, so we turned them on for all 46 we own.
- **CI quietly used old Node.** A GitHub Actions job that asks for Node `'24'` gets whatever 24.x the runner image already has cached unless you add `check-latest: true`. On October 27 some runners still cached 24.21.0 and 22.23.3, both unpatched. One job that pulls news and filings from outside sites ran on 24.21.0 that afternoon.
- **Builds on dead Node.** Four sites still built on Node 20, which reached end of life and got no fix this time.
- **Old build dependencies.** Twenty of our static site repos locked template libraries with critical or high advisories. They only run at build time on our own templates, so the risk was low, but they were overdue. `npm audit fix` cleared them without a single change to the built pages, which we confirmed by building each site before and after and comparing every file.

For pulling dozens of local clones up to date, we use a rule that has not burned us yet. Fetch everything first and change nothing. Then fast forward a clone only when it has an upstream branch, nothing local that is not pushed, no uncommitted changes, and no file touched in the last 90 minutes. Anything that fails a test gets skipped and listed, never reset. Clones that live services run from are left to their own update process. That rule moved 64 clones forward without touching a single piece of work in progress.

## Help yourself: a checklist for tonight

Most of this takes ten minutes per machine. If you set up your machine with our [AI Terminal Kickstart](/blog/blog-ai-terminal-kickstart/), re-run it: as of October 27 it checks the Node and Git for Windows you already have against these fixes and offers the update.

1. **Every Node you run.** On each laptop, server and build machine run `node -p process.version`. With nvm, `nvm install 24.22.1` (or `22.23.4`) installs the fix next to your current version. Restart long running services afterwards, because a running process keeps the old binary loaded until it restarts.
2. **Services pinned to a path.** Search your service files for a hard coded Node path, for example `grep -r "versions/node" ~/.config/systemd`. A unit that points at the old version stays vulnerable after you upgrade.
3. **Serverless functions.** Deploy a probe like the one above to a throwaway site or a draft, and read what it reports. On Netlify, set `AWS_LAMBDA_JS_RUNTIME` to a patched line and redeploy if your functions fetch outside content. Other hosts have their own runtime setting; the probe works the same everywhere.
4. **Python.** Run `python3 -m pip show brotli brotlicffi` in every virtual environment and container you care about. If either shows up below 1.2.1, upgrade it.
5. **Containers.** Rebuild or pull fresh images. Updating the host does not touch the libraries inside a container.
6. **Windows.** Check `node -v`, `git --version` and `pwsh -v`. You want Git 2.56.0.windows.4 or later and PowerShell 7.4.22, 7.5.13 or 7.6.9 or later. Some command line tools bundle their own Node, so update those too.
7. **CI.** Add `check-latest: true` under `actions/setup-node`, or pin an exact patched version.
8. **Alerts.** Turn on Dependabot alerts, or whatever your platform offers, so the next one finds you instead of the other way around.

## Why bother when nobody is targeting you

Nobody needs to be targeting you. Tools that fetch other people's pages are exactly what attackers like to feed, and bugs like this get scanned for in bulk within days. The fix costs a few minutes per machine. Cleaning up after someone runs code inside your server costs a great deal more.

The bigger lesson for us was about ownership. We patched every runtime we installed within hours. The one we missed was a runtime we never installed, picked by a platform default, patched on someone else's schedule, and invisible to every file in our repos. If your site runs code you did not install, put a probe on your checklist next to `npm audit`.

If you are building and running your own small sites instead of paying an agency to babysit them, this is the habit that keeps that choice cheap. My book *The $97 Launch* covers the rest of that do it yourself path, from first deploy to keeping things running.

## Related reading

- [LiteLLM 1.107.0 Ran When Python Started](/blog/litellm-1-107-0-compromise-ioc-checks/): the supply chain check we now run before every server update.
- [Updated on Disk, Old in Memory](/blog/safe-update-command-self-hosted-ai-server/): why a patched package does nothing until the process using it restarts.
- [Auditing Serverless Functions for the 25 Posture Failures](/blog/blog-tool-serverless-posture-audit/): the wider list of things to check in your functions.
- [Audit Your Own Web App's Security Before You Pay Anyone](/blog/audit-your-web-app-security-yourself/): a do it yourself security pass for small teams.
- [AI Terminal Kickstart](/blog/blog-ai-terminal-kickstart/): the setup scripts, which now flag an unpatched Node or Git for Windows.
- [Nation-State Malware, Protestware, and the Packages Your AI Pulls In](/blog/blog-supply-chain-malware-ai-dependencies/): the dependency side of keeping repos current.

## Fact-check notes and sources

All read on October 27, 2026 (UTC).

- **Node.js fixes, versions and the other nine CVEs:** Node.js, [October 2026 security releases](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases): CVE-2026-111497, fixed in 22.23.4, 24.22.1 and 26.12.1, released October 21; Node 20 and other end of life lines not fixed.
- **Brotli library releases and affected range:** Google, [brotli releases](https://github.com/google/brotli/releases) (1.0.10, 1.1.1 and 1.2.1, October 21) and F5, [K000163902](https://my.f5.com/manage/s/article/K000163902) (affected 1.0.3 to 1.2.0).
- **Python packages:** GitHub Advisory Database, [GHSA-9v8h-qqgm-4m35](https://github.com/advisories/GHSA-9v8h-qqgm-4m35) (brotli below 1.2.1, brotlicffi below 1.2.1.0) and [brotlicffi on PyPI](https://pypi.org/project/brotlicffi/); the fixed wheels were uploaded October 22.
- **Bun:** Bun, [GHSA-663w-cv8g-jj2p](https://github.com/oven-sh/bun/security/advisories/GHSA-663w-cv8g-jj2p), published October 23: affects 1.0.21 up to 1.4.4; compiled executables must be rebuilt.
- **.NET, PowerShell and Visual Studio:** .NET, [announcement 494](https://github.com/dotnet/announcements/issues/494) and the [October 2026 out of band update post](https://devblogs.microsoft.com/dotnet/dotnet-october-2026-oob-security-updates/); PowerShell [releases](https://github.com/PowerShell/PowerShell/releases) 7.4.22, 7.5.13 and 7.6.9 built on the patched SDKs; Visual Studio 2022 [release notes](https://learn.microsoft.com/en-us/visualstudio/releases/2022/release-notes) for 17.14.43.
- **Git for Windows:** [Git for Windows releases](https://github.com/git-for-windows/git/releases): 2.56.0.windows.4 (October 26) moves its bundled Brotli to 1.2.1.
- **Ubuntu:** Canonical, [USN-8956-1](https://ubuntu.com/security/notices/USN-8956-1) and the [CVE-2026-111497 tracker](https://ubuntu.com/security/CVE-2026-111497).
- **Cloudflare:** a maintainer comment on [workerd issue 8004](https://github.com/cloudflare/workerd/issues/8004) on October 27 that production is patched (a comment on an open issue, not a formal advisory), and the [October 25 emergency WAF release](https://developers.cloudflare.com/changelog/post/2026-10-25-emergency-waf-release/), log mode by default.
- **Netlify runtime selection:** Netlify, [functions optional configuration](https://docs.netlify.com/build/functions/optional-configuration/) (`AWS_LAMBDA_JS_RUNTIME`, not settable in `netlify.toml`) and the [July 2026 Node 24 default changelog](https://www.netlify.com/changelog/2026-07-07-nodejs-24-default-new-sites/).
- **Deno:** Deno's dependency lock file lists the Rust `brotli` and `brotli-decompressor` crates and no binding to Google's C library; that the Rust port is unaffected comes from secondary write ups, such as Tenable's [FAQ](https://www.tenable.com/blog/brotli2shell-frequently-asked-questions-for-google-brotli-decoder-cve-2026-111497), not from a Deno advisory.
- **undici behavior and version strings:** read from the Node v24.22.1 source tree (`deps/undici/src/lib/web/fetch/index.js`: Brotli advertised by default on HTTPS, decoding chosen from the response's `Content-Encoding`), and from `process.versions` on patched builds (24.22.1 still reports Brotli 1.2.0; 22.23.4 reports 1.1.0).
- **Our runtimes and sites:** our own probe responses on October 27, 2026 (`nodejs24.x` reported v24.21.0 in us-east-2; `nodejs22.x` reported v22.23.4), the runtime field in each site's live Netlify deploy record, and the GitHub Actions logs that showed Node 24.21.0 and 22.23.3 on October 27 runs.

*This post is informational, not security consulting or incident response advice. Mentions of Google, Node.js, Netlify, Amazon Web Services, Cloudflare, Microsoft, GitHub, Canonical, the Python Package Index, Bun, Deno, F5 and Tenable are nominative fair use. No affiliation is implied.*


---

Canonical HTML: https://jwatte.com/blog/brotli-cve-2026-111497-patch-check/
RSS: https://jwatte.com/feed.xml
JSON Feed: https://jwatte.com/feed.json
Hero image: https://jwatte.com/images/brotli-cve-2026-111497-patch-check.webp
