Every fact used in the companion post, with where it comes from. Times are UTC. Checked on 2026-10-22 unless noted.
| Fact | Source |
|---|---|
| Node.js released security updates for the 26.x, 24.x and 22.x lines on Wednesday, 21 October 2026: v22.23.4, v24.22.1 and v26.12.1. "The highest severity issue fixed in this release is HIGH." | Node.js advisory |
| "It's important to note that End-of-Life versions are always affected when a security release occurs." | same advisory |
| Dependency updates in the release: c-ares (26, 24, 22), "ca-cert (eMudhra Intermediate CA Compromise)" (26, 24, 22), OpenSSL (24, 22), SQLite (22), Undici (26, 24, 22). | same advisory |
| GitHub releases v26.12.1, v24.22.1 and v22.23.4 were published at 10:38:29, 10:38:25 and 10:38:20 UTC on 21 October. | gh api repos/nodejs/node/releases/tags/<tag> (published_at) |
SHASUMS256.txt and the linux-arm64 tarballs are dated 21-Oct-2026 10:32 (22.23.4), 10:34 (24.22.1) and 10:36 (26.12.1) in the dist listings. |
https://nodejs.org/dist/v22.23.4/ , /v24.22.1/ , /v26.12.1/ |
Small discrepancy: https://nodejs.org/dist/index.json dates v22.23.4 and v24.22.1 as 2026-10-20 and v26.12.1 as 2026-10-21; the changelogs and advisory say 2026-10-21. |
index.json, changelogs |
| CVE | Title in the advisory | Severity | Lines |
|---|---|---|---|
| CVE-2026-111497 | Node.js Brotli decoder out-of-bounds write permits remote code execution | high | 26, 24, 22 |
| CVE-2026-105483 | TLS certificate hostname bypass when the IP is written fullwidth digits | high | 26, 24 |
| CVE-2026-106069 | TLS certificate hostname bypass when the host loosely resolves to an IP address | high | 26, 24, 22 |
| CVE-2026-105482 | dns.resolve truncates names containing NUL char | high | 26, 24, 22 |
| CVE-2026-105481 | Keep-alive socket process a response that was written before the request was sent | medium | 26, 24, 22 |
| CVE-2026-106072 | Improper certificate validation for non-canonical IPv4 hostnames allows DNS SANs to authenticate IP endpoints | medium | 26, 24, 22 |
| CVE-2026-106074 | crypto.randomUUID() and randomInt() entropy caches are saved into user-land startup snapshots and SEAs, so every process repeats the same values | medium | 26, 24, 22 |
| CVE-2026-106070 | Node.js ZipFile returns attacker-controlled prefix bytes after failed ZIP header validation | low | 26 |
| CVE-2026-106071 | Permission Model filesystem over-grant via wildcard radix lookup | low | 26 |
| CVE-2026-106073 | Permission Model bypass with fs.realpathSync | low | 26, 24, 22 |
The advisory body for CVE-2026-111497 reads: "A flaw in Node.js can cause Node.js Brotli decoder out-of-bounds write permits remote code execution." It does not say whether the bug has been exploited, and it gives no technical detail. Neither does this repo.
From doc/changelogs/CHANGELOG_V22.md, CHANGELOG_V24.md and CHANGELOG_V26.md in nodejs/node:
- All three list two CVE-2026-111497 entries: a cherry-pick of an upstream Brotli commit into
deps/brotli, and a matching change in Node's zlib binding. - All three list "crypto: update root certificates to NSS 3.131.1" and "tls: reject chains containing blocked public keys".
- 24.22.1 and 26.12.1 list "(CVE-2026-105483) tls: fix ip normalization regression". 22.23.4 does not, which matches the advisory (26 and 24 only).
- All three list "(CVE-2026-106069) tls: reject non-canonical IP addresses". Only the 26.12.1 entry also names CVE-2026-106072 on that line, although the advisory says CVE-2026-106072 affects 22 and 24 too.
- "(CVE-2026-105482) dns: validate resolve inputs for null bytes" in all three.
- Bundled undici: 6.29.1 (22.23.4), 7.30.1 (24.22.1), 8.12.2 (26.12.1). OpenSSL 3.5.9 in 22.23.4 and 24.22.1. c-ares 1.34.9 in all three. SQLite 3.53.4 in 22.23.4.
- 26.12.1 also reverts "module: synchronously load most ES modules" (commit e5cae10148).
- At 2026-10-22T06:01Z the CVE Services API returned
"state":"RESERVED"for all ten IDs (https://cveawg.mitre.org/api/cve-id/<id>), andCVE_RECORD_DNEfor the records. NVD (services.nvd.nist.gov/rest/json/cves/2.0?cveId=<id>) returned no results. So there are no published CVE records and no CVSS scores yet; the severities above are the Node.js project's.
- The cherry-picked commit is in the upstream Brotli decoder (google/brotli, February 2026). We leave out its details here on purpose.
- Brotli published v1.2.1, v1.1.1 and v1.0.10 on 2026-10-21 between 19:26:58 and 19:27:34 UTC. Each release note says: "SECURITY - CVE-2026-111497: out-of-bounds write permits remote code execution".
- Debian builds Node against the system Brotli:
debian/rulesof nodejs 20.19.2+dfsg-1+deb13u2 (trixie) passes--shared-brotli(and--shared,--shared-zlib,--shared-openssl), anddebian/controlbuild-depends onlibbrotli-dev(sources.debian.org). For such builds the Brotli fix comes from the distribution's brotli package. - At 2026-10-22 ~06:09Z the Debian security tracker had no page for CVE-2026-111497 (HTTP 404) and Ubuntu's notices API returned 0 notices for it.
fetch()in Node is undici: "The implementation is based upon undici" (doc/api/globals.mdat v24.22.1).- undici's fetch decodes
Content-Encoding: brresponses withzlib.createBrotliDecompress(require('node:zlib')): deps/undici/src/lib/web/fetch/index.js at v24.22.1, and the same code at v22.23.4 (line 2164) and v26.12.1 (line 2322). - The decoders are chosen from the response's
Content-Encodingheader only. The request'sAccept-Encoding(br, gzip, deflatefor https,gzip, deflatefor http, same file lines 1523-1528) is not checked against it, so a server can send Brotli to a client that did not ask for it. - axios in Node decompresses
brresponses unlessdecompress: false: lib/adapters/http.js at v1.20.0. - body-parser 2.0.0 (2024-09-10) "Add brotli support" (HISTORY.md);
inflate"Defaults totrue" (README); lib/read.js at v2.3.0 decompressesbrrequest bodies and returns 415 wheninflate: false. Express 5.3.0 depends onbody-parser ^2.3.0and exportsexpress.json = bodyParser.json. body-parser 1.20.8 (Express 4) handles only deflate, gzip and identity. - The decoder is compiled into the
nodebinary (deps/brotli), so updating an npm package such asundicidoes not change it.
- Node release schedule (nodejs/Release schedule.json): v20 ended 2026-04-30, v25 ended 2026-06-01; v22 ends 2027-04-30, v24 ends 2028-04-30; v26 becomes LTS on 2026-10-28.
- docker-library/official-images commit 74ff70c42b, "Node: feat: Node.js 22.23.4, 24.22.1, 26.12.1", committed at 2026-10-21T10:48:56Z. The
library/nodefile no longer lists tags for 20 or 25. - Our scan of 24 official
nodetags for linux/amd64 and linux/arm64, from registry metadata only:results/registry-20261022T062338Z.json, summary inresults/registry-20261022T062338Z-summary.txt. Docker Hub's per-imagelast_pushedis metadata, not proof of when a tag first served an image.
- pm2 docs, Startup: "When you upgrade the local Node.js version, be sure to update the PM2 startup script, so it runs the latest Node.js binary you have installed", with
pm2 unstartupthenpm2 startup; and "save the app list so it will respawn after reboot: pm2 save". - pm2 docs, Updating PM2: "update the in-memory PM2 daemon via command: pm2 update".
- pm2 6.0.14 CLI:
-a --update-env"force an update of the environment with restart/reload" (lib/binaries/CLI.jsline 49). - pm2 6.0.14
lib/Common.jsresolveInterpreter: a.jsscript gets the interpreternode(fromlib/API/interpreter.json), resolved through the app's environment when spawned; a script without a known extension that is not a binary getsprocess.execPath, the absolute path of the Node running the pm2 CLI.lib/God/ForkMode.jsspawns the interpreter with the app's saved environment. - pm2 cluster mode:
lib/God/ClusterMode.jscallscluster.fork(), and Node's cluster primary forks workers withchild_process.fork()(lib/internal/cluster/primary.jsat v24.22.1), which defaultsexecPathtoprocess.execPath(lib/child_process.jsline 144). So cluster-mode workers run on the daemon's Node whatever the entry's interpreter or PATH says. The lab shows it: theclusteredapp stayed on v24.14.0 afterrestart --update-envand moved only afterpm2 update. - nvm README: "New shells will start with the default version of node (e.g.,
nvm alias default)", andnvm install --reinstall-packages-from=...to move global npm packages to the new version. results/pm2-lab-20261022T063002Z.txt: the lab that shows each of these on a real pm2 6.0.14 with Node v24.14.0 and v24.22.1 (lab/pm2-pin-lab.sh), four apps: barenode,npm start, an absolute--interpreterand cluster mode.
-
results/pgrep-vs-proc-20261022T063334Z.txt: on the Pi,pgrep -c nodeandpgrep -c -x nodeboth returned 0 while five processes ran anodebinary, namedPM2 v6.0.14: Go,MainThreadandnext-server (v1in/proc/<pid>/comm. -
examples/run-20261022T071033Z.txtand.json:node_runtime_audit.py --redacton the Raspberry Pi we build on, 2026-10-22T07:10:33Z, as the user that owns the Node processes. The script refused to run thepm2CLI because the file is world-writable (mode 777), so the live pm2 list is skipped and reported as unknown. -
results/pm2-path-20261022T070504Z.txt: on the same Pi, the saved pm2 list (dump.pm2) and the process the pm2 daemon started (/proc/<pid>/environ) both have the v24.14.0 install first onPATH. -
On the same Pi, the first line of the app's
node_modules/.bin/nextis#!/usr/bin/env node(checked withhead -1), which is whybun run devends up starting Node processes. -
The fnm
defaultalias on the Pi was changed to v24.22.1 at 06:03 UTC on 22 October (the symlink's mtime); the pm2 daemon kept running v24.14.0.
The production upgrade in the post was done on 21 October on a small box running six Node apps under pm2, installed with nvm. These details come from that work and are generalized (no hosts, users or products named):
- nvm installed the new version next to the old one, and the old one was kept for rollback.
- Several pm2 entries had the old Node binary as an absolute interpreter path, and npm/npx scripts under the old version's
bindirectory; they were deleted and recreated. Entries with a barenodeinterpreter moved afterpm2 restart <name> --update-envfrom a shell with the new Node first inPATH. - The pm2 systemd unit had the old version's
PATHand pm2 path hard-coded; after editing it,pm2 updatemoved the daemon and restarted every app, and the sites were down for under a minute while the Next.js apps started. - Checking
/proc/<pid>/exefound child processes (npxtotsxtonode,npm starttonext-server) that each needed checking. - Cron jobs and helper scripts on two other boxes had absolute paths to the old Node.
The lab (lab/pm2-pin-lab.sh) reproduces the pm2 behaviors on a separate machine. It measures the duration of the pm2 update command, not service availability.