Skip to main content
V Vatsal Trivedi

The October 2026 Node.js bugs, and how to patch them yourself

trivedi-vatsal 6 min read

If a service is on Node 22, 24, or 26 and it is not on the October 21 point release, it is missing this fix. The patched builds are 22.23.4, 24.22.1, and 26.12.1. Node 18, 20, 21, 23, and 25 will not get a build for this release. Those lines are already end of life, and the Node.js project treats end-of-life versions as affected whenever a security release goes out.

The prompt

Paste this into an agent running on the repo. It walks through the whole October 21 release, step by step. Put your own GitHub login and email in the author lines, or the commit gets signed with whatever global git identity your work laptop has.

Security scan promptPreview

Scan this repo with the advisory payload at the end of this prompt. If a newer payload is attached after this prompt, use that payload instead of the default one.

Author every commit as the identity in the payload. Set it for that commit only (git -c user.name=... -c user.email=...). Do not change git config. Before opening a pull request, verify the pushed commit's author and committer are that name and email, and that GitHub attributes the commit to that account.

Push and open pull requests as the GitHub account that owns the repo. If the active gh account gets a 403, switch to the account that can push, do the push and pull requests, then switch the active account back.

What to scan

The workspace may be a folder of repos. A directory with its own .git is a repo. Scan each repo that deploys. Do not treat the folder as one repo, and do not invent a git repo at the workspace root.

For each repo:

  1. git fetch --all --prune, then list git ls-remote --heads origin and the default branch (origin/HEAD).
  2. Read deploy config: GitHub workflows, Dockerfiles, compose files, Helm, Kubernetes, fly.toml, render.yaml, railway, Procfile, app specs, and package-manager files.
  3. A long-lived deploy branch is a branch the repo actually builds or deploys from. Count the default branch when a workflow deploys on push to it. Count any other branch named in a deploy workflow, release branch, or environment branch. Decide from the repo. Do not assume the names prod or develop.
  4. Do not count these as deploy sources unless a deploy workflow really builds from them: gh-pages and other publish-output branches, dependabot/*, feature branches, agent branches, and open pull-request branches.

How to change it

Open one pull request per deploy branch. Do not combine branches into one pull request.

Branch from the remote tip of that base (git checkout -B FIX origin/BASE). Do not branch from a stale local checkout. Push with an explicit refspec to the fix branch (git push -u origin HEAD:FIX). Never push to the base branch.

If two deploy branches have the same lockfile, cherry-pick the lockfile commit. If the lockfiles differ, repeat the audit on each branch.

Node.js

Use the patched releases in the payload. Node 20 and older have no fix.

Search every place that selects the runtime: Dockerfile stages, compose image: lines, .nvmrc, .node-version, actions/setup-node, other workflow node-version inputs, engines.node, devcontainers, and Nix.

Image rule, applied to every stage, including the app image and any worker target:

  • Already on Node 26: pin 26.12.1 (or the payload's 26.x release).
  • Already on Node 24: pin 24.22.1. Use node:24.22.1-alpine only in this case.
  • On Node 22 below the patched 22.x release, or on Node 20 or older, or on a floating tag (node:20, node:latest, node:22): pin 22.23.4. For an image, use node:22.23.4-alpine when the current tag is alpine or has no OS variant.
  • If the current image is not alpine (slim, bookworm, Debian), bump only the version and keep that variant. Do not switch the base OS.
  • If the repo has no Dockerfile and no image, do not create one. Pin the CI version, .nvmrc, or other selector that actually builds the repo.

PHP

Search for a PHP runtime, composer.json, composer.lock, a PHP image, or .php-version.

If none of those exist, say so and do not add PHP. If they exist, bump to the payload's patched release on the same major line, in the same pull request as the Node.js change for that branch.

npm audit

Run npm audit only on a branch that has package-lock.json.

Run npm audit fix without --force. Reuse the install flags this repo already uses (.npmrc, CI, Docker), such as --legacy-peer-deps.

Commit lockfile updates that stay inside the current package.json ranges. Do not rewrite direct dependency specifiers. Do not take npm audit fix --force.

If a finding remains only because a parent pins an old transitive and a same-major patched release exists, add an npm override to that patched release. Honor the payload's special cases. In particular, postcss-selector-parser is vulnerable below 7.1.6, so override it to 7.1.6, not 6.1.5.

Do not apply a fix on the payload's do-not-apply list. That includes Prisma 6 to 7, OpenTelemetry auto-instrumentations 0.78 to 0.82, braces (no official patch), and an eslint-config-next downgrade.

If the branch uses pnpm or Yarn and has no package-lock.json, do not create one. Apply the same override rule with that package manager's override field, and refresh its lockfile. Do not run an update that rewrites direct ranges.

Pull request body

For each pull request, list:

  • what was patched (runtime pins and lockfile packages)
  • what is still open, and why (no same-major release, outside the current range, on the do-not-apply list, or no lockfile for npm)
  • images: if this repo builds an app image and any worker image, say both need a rebuild after merge. If it has no image, say there is no app image and no worker image to rebuild, and that the next deploy run has to use the patched runtime.

Report back

End with the pull request URLs, the deploy branches you kept, the branches you rejected and why, and anything you refused to invent (PHP, Docker, a lockfile, a major upgrade).


Advisory payload (October 21, 2026)

Replace this section when a later security release is the one to scan for.

  • Author name: your-github-login
  • Author email: you@example.com

Node.js

Source: https://nodejs.org/en/blog/vulnerability/october-2026-security-releases

Patched releases: 22.23.4, 24.22.1, 26.12.1. Node 20 and older get no fix.

High severity issues named for this scan:

  • CVE-2026-111497, Brotli decoder out-of-bounds write. Affects 26.x, 24.x, 22.x. https://www.cve.org/CVERecord?id=CVE-2026-111497
  • CVE-2026-105482, DNS NUL truncation. Affects 26.x, 24.x, 22.x. https://www.cve.org/CVERecord?id=CVE-2026-105482
  • CVE-2026-105483, TLS hostname bypass with fullwidth digits. Affects 26.x and 24.x. https://www.cve.org/CVERecord?id=CVE-2026-105483
  • CVE-2026-106069, TLS hostname bypass for non-canonical IPs. Affects 26.x, 24.x, 22.x. https://www.cve.org/CVERecord?id=CVE-2026-106069

The release also fixes CVE-2026-105481, CVE-2026-106072, CVE-2026-106074, CVE-2026-106070, CVE-2026-106071, and CVE-2026-106073. Pinning the patched release covers those too.

PHP

Source: https://thephp.foundation/blog/2026/10/21/todays-php-security-release-and-how-were-strengthening-php-security/

Patched releases, same major line only: 8.5.12, 8.4.27, 8.3.36, 8.2.35.

  • CVE-2026-109618, RCE via OOM corruption. https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w and https://www.cve.org/CVERecord?id=CVE-2026-109618
  • CVE-2026-109619, RCE via stale global state. https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp and https://www.cve.org/CVERecord?id=CVE-2026-109619

npm do-not-apply

  • Prisma 6 to 7
  • OpenTelemetry auto-instrumentations 0.78 to 0.82
  • braces: no official patch
  • eslint-config-next downgrade suggested by audit: wrong, do not take it
  • postcss-selector-parser: vulnerable below 7.1.6. Override to 7.1.6, not 6.1.5

The rest of this post is what the prompt does, so you can check its work or do it by hand.

The primary source is the October 21 security release. I also keep this checker open while I triage. You paste a version, an image tag, or a whole Dockerfile, and it tells you which of these CVEs apply and which tag to switch to. It runs in the browser and never looks at your servers, so node -v on the running process is still the check that closes the ticket.

What is broken

Ten CVEs shipped together: four high, three medium, three low. The point release is the fix for all of them. These are the four high ones.

CVE-2026-111497 is a Brotli decoder bug. It can write past the end of a buffer, and Node describes that as a path to remote code execution. It affects 22, 24, and 26. If something you run decompresses data from the network, this is the one I would move first.

CVE-2026-105482 is in dns.resolve. A name that contains a NUL byte gets truncated. 22, 24, and 26.

CVE-2026-105483 is a TLS hostname check that can be bypassed when an IP is written with fullwidth digits. The advisory lists this one for 24 and 26.

CVE-2026-106069 is the other TLS hostname bypass, for a host that only loosely resolves to an IP. 22, 24, and 26.

The same builds also pick up new c-ares and Undici, a CA certificate store update (the eMudhra intermediate was compromised), OpenSSL on 22 and 24, and SQLite on 22. You get those by moving to the point release. You do not get them by editing package.json.

Which tag to switch to

What you have nowPin this
22.x below 22.23.4, or a floating node:22 tag22.23.4, image node:22.23.4-alpine
24.x below 24.22.1, or a floating node:24 tag24.22.1, image node:24.22.1-alpine
26.x below 26.12.1, or a floating node:26 tag26.12.1, image node:26.12.1-alpine
18, 20, 21, 23, 25, node:20, node:lts from before the release22.23.4 / node:22.23.4-alpine

Stay on the major you already run when that major has a fix. A 24 app should not be dragged back to 22, and it should not be jumped to 26 in the same afternoon as a security patch. A 20 app has no patched 20. Move it to 22.23.4.

If the image is bookworm, slim, or Debian rather than Alpine, change the version and keep the variant. node:24-bookworm becomes node:24.22.1-bookworm. Swapping the base OS to Alpine will break native modules that were fine yesterday.

node:22-alpine is not a pin. It means “whatever 22 was when this image was last pulled.” An image built on October 20 is still the vulnerable build, even if the tag on Docker Hub has moved. Pull again, read node -v, then pin that exact version. The checker spells this out in its notes on floating tags.

Find the version that is actually running

The engines field in package.json only records what someone intended. The version that matters is whatever the running process reports.

On a host or inside a container:

node -v

On an image you have not deployed yet:

docker run --rm --entrypoint node IMAGE -v

In the repo, search for the selectors, not the word “node”:

  • every FROM node: in every Dockerfile, including the worker stage
  • image: lines in Compose
  • .nvmrc and .node-version
  • node-version in GitHub Actions
  • the Node image your CI builds with, which is often older than the app image

A repo can be “fixed” in the Dockerfile and still build on Node 20 in Actions. Both have to move.

The edit

Change the app stage and the worker stage. I missed a worker target once and shipped the patch to the API while the queue kept running the old image.

# before
FROM node:20-alpine

# after
FROM node:22.23.4-alpine
# before, already on 24
FROM node:24-bookworm

# after
FROM node:24.22.1-bookworm

For a repo that has no image, do not add a Dockerfile so the patch looks complete. Change the version the build uses.

# .github/workflows, or whatever file calls setup-node
node-version: 22.23.4
# .nvmrc
22.23.4

Then rebuild. A new Node binary on your laptop does not rewrite an image that is already in the registry. After the new image is running, node -v has to print v22.23.4, v24.22.1, or v26.12.1. Paste that output back into the checker if you want a second opinion, but treat it as a triage aid. If it ever disagrees with the advisory, the advisory wins.

A few cases where the file looks patched and the service is not:

  • The image was not rebuilt, or it was rebuilt without pulling the new base.
  • One stage, usually the worker, is still on 20.
  • CI still installs Node 20.
  • The Node binary came from apt, apk, or a Red Hat UBI image. Those use their own version numbers and sometimes the system OpenSSL. Follow that vendor’s advisory, then confirm with node -v.
  • You updated Node, and left an undici package in package.json on the old release. Node’s bundled Undici moves with the runtime. A direct dependency does not.

Do this on the branch that deploys

I have a site that deploys when main is pushed and publishes the files to gh-pages. Patching gh-pages changes nothing about the next build. Read the workflow. The branch in on.push.branches is the one to patch. Do not assume it is called prod or develop.

Open one pull request for each branch that deploys. Start it from the remote tip of that branch. A local checkout that is a week behind will drag unrelated commits into the patch.

In the pull request, say which images have to be rebuilt after merge, or say that there is no image. Don’t write “rebuild the image” on a repo that doesn’t have one.

PHP shipped the same day

Skip this part if the repo doesn’t run PHP. And don’t add a PHP image to a Node repo because the two announcements landed together.

PHP 8.5.12, 8.4.27, 8.3.36, and 8.2.35 fix two remote code execution bugs: CVE-2026-109618 (memory corruption if a request hits memory_limit at the wrong moment) and CVE-2026-109619 (stale engine state left behind after a fatal error). The PHP Foundation writeup is the one to read. Stay on the same major. An 8.3 service goes to 8.3.36.

Once the pull request is open, run node -v yourself on whatever you deploy. I still go back to the advisory when something looks off, and to the checker when I just want a quick look at a tag.