Skip to content

Commit e4a2ab1

Browse files
committed
Node runtime audit for the October 2026 Node.js security release
0 parents  commit e4a2ab1

22 files changed

Lines changed: 3441 additions & 0 deletions

‎.github/workflows/test.yml‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: test
2+
on:
3+
push:
4+
branches: [main]
5+
pull_request:
6+
permissions:
7+
contents: read
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
matrix:
13+
python: ["3.8", "3.12"]
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: actions/setup-python@v5
17+
with:
18+
python-version: ${{ matrix.python }}
19+
- run: python -m unittest discover -s tests -v
20+
- name: run against the runner itself (any verdict is fine, a crash is not)
21+
run: |
22+
set +e
23+
python node_runtime_audit.py --no-containers > out.txt
24+
code=$?
25+
cat out.txt
26+
grep -q '^result:' out.txt && test "$code" -le 2

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__pycache__/
2+
*.pyc

‎LICENSE‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 The DevOps Daily
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎README.md‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# node-runtime-audit
2+
3+
```bash
4+
curl -fsSLO https://raw.githubusercontent.com/The-DevOps-Daily/node-runtime-audit/main/node_runtime_audit.py
5+
sudo python3 node_runtime_audit.py # or without sudo: your own processes only
6+
```
7+
8+
Patching Node is a one-line version bump. Finding every Node your servers actually run is the work. This script lists the Node binary behind every running process and every place that will start Node later, and checks each one against the [Node.js security release of 21 October 2026](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases) (fixed in 22.23.4, 24.22.1 and 26.12.1; every end-of-life line counts as vulnerable).
9+
10+
It is one file of standard-library Python (3.8+, Linux). It only reads: it never restarts, edits or upgrades anything.
11+
12+
Written for the DevOps Daily post: https://devops-daily.com/posts/node-october-2026-security-release-find-every-node
13+
14+
## What it checks
15+
16+
| Where | How |
17+
| --- | --- |
18+
| Running processes | `readlink /proc/<pid>/exe` for the real binary, then the version compiled into that file (read through `/proc/<pid>/exe`, so it works for deleted binaries and for processes inside containers). `--version` is also run when the file can only be changed by root or you, and the two must agree. |
19+
| pm2, live | `pm2 jlist` for your own `PM2_HOME` when its daemon is running: the daemon's binary, each entry's interpreter and script when they point into one Node install, the first `node` on the entry's saved `PATH` (or the daemon's Node for cluster mode, which forks workers from the daemon), and the Node processes really running under the entry. |
20+
| pm2, saved | `dump.pm2`, the list `pm2 resurrect` and a reboot start. |
21+
| Version managers | nvm, fnm, volta, asdf, mise, nodenv and n installs, and the default alias. |
22+
| Crontabs, systemd units | Absolute paths to `node`, `npm`, `npx` and anything inside a versioned Node install (`.../v24.14.0/...`), including `PATH=` lines and `pm2 startup` units. |
23+
| Other files | `--scan-path DIR` searches scripts for the same paths. |
24+
| Containers | `docker exec <id> node --version` (or podman) in each running container, plus `NODE_VERSION` from the image config. |
25+
26+
Exit code: **1** if anything runs or will start a vulnerable Node, else **2** if anything could not be checked, else **0**. Unknown is never reported as patched. Distribution builds that link the system Brotli library (Debian's `nodejs`) are reported as unknown, because the fix for them comes from the distribution's packages, not from the version number.
27+
28+
```bash
29+
python3 node_runtime_audit.py --json # machine-readable
30+
python3 node_runtime_audit.py --redact # hide user names, home dirs and pm2 app names
31+
python3 node_runtime_audit.py --scan-path ~/bin # also search your helper scripts
32+
python3 node_runtime_audit.py --fixed 22=22.23.4,24=24.22.1,26=26.12.1 # the default; change it for the next release
33+
```
34+
35+
Run it on many hosts without copying it first:
36+
37+
```bash
38+
for h in web1 web2; do ssh "$h" 'sudo python3 - --json' < node_runtime_audit.py > "node-$h.json"; done
39+
```
40+
41+
## The other files
42+
43+
- `registry_scan.py`: which official `node` image tags ship a fixed Node, read from registry metadata only (manifest and config by digest, no layers). `scripts/summarize_registry.py` turns a scan into hours after the release.
44+
- `lab/pm2-pin-lab.sh OLD_BIN NEW_BIN`: starts four pm2 apps (bare `node`, `npm start`, an absolute `--interpreter`, cluster mode) in a throwaway `PM2_HOME` on the old Node, switches `PATH` to the new one and records which binary each process really runs after `restart`, `restart --update-env`, delete and recreate, and `pm2 update`.
45+
- `scripts/record-run.sh`: records a redacted run under `examples/`.
46+
- `tests/`: `python3 -m unittest discover -s tests -v`. Fake hosts with the cases a version check gets wrong: a deleted binary still running, a container binary, a distribution build, a pm2 entry whose config is fixed but whose process was never restarted, `npm` pinned to the old install while its child gets the new one, end-of-life and pre-release versions, cron jobs on a removed version.
47+
48+
## Results
49+
50+
- `results/pm2-lab-20261022T063002Z.txt`: pm2 6.0.14 with Node v24.14.0 and v24.22.1.
51+
- `examples/run-20261022T062905Z.txt`: the script on our Raspberry Pi.
52+
- `results/registry-20261022T062338Z.json`: 24 official tags, amd64 and arm64.
53+
54+
Facts and their sources are in [SOURCES.md](SOURCES.md).
55+
56+
## Limits
57+
58+
- Linux only (`/proc`). Not root: other users' processes and crontabs are not visible, and the script says so.
59+
- A Node binary renamed to something other than `node` or `nodejs` (single executable applications, Electron) is not detected as a process.
60+
- `pm2 jlist` is only run for your own `PM2_HOME`, so other users' live pm2 environments are not read; their saved lists and processes are.
61+
- Kubernetes is not covered. Stopped containers and images that are not running are not covered.
62+
63+
MIT licensed.

‎SOURCES.md‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Sources and fact sheet
2+
3+
Every fact used in the companion post, with where it comes from. Times are UTC.
4+
Checked on 2026-10-22 unless noted.
5+
6+
## The release
7+
8+
| Fact | Source |
9+
| --- | --- |
10+
| 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](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases) |
11+
| "It's important to note that End-of-Life versions are always affected when a security release occurs." | same advisory |
12+
| 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 |
13+
| 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`) |
14+
| `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/ |
15+
| 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 |
16+
17+
### The vulnerabilities (from the advisory)
18+
19+
| CVE | Title in the advisory | Severity | Lines |
20+
| --- | --- | --- | --- |
21+
| CVE-2026-111497 | Node.js Brotli decoder out-of-bounds write permits remote code execution | high | 26, 24, 22 |
22+
| CVE-2026-105483 | TLS certificate hostname bypass when the IP is written fullwidth digits | high | 26, 24 |
23+
| CVE-2026-106069 | TLS certificate hostname bypass when the host loosely resolves to an IP address | high | 26, 24, 22 |
24+
| CVE-2026-105482 | dns.resolve truncates names containing NUL char | high | 26, 24, 22 |
25+
| CVE-2026-105481 | Keep-alive socket process a response that was written before the request was sent | medium | 26, 24, 22 |
26+
| CVE-2026-106072 | Improper certificate validation for non-canonical IPv4 hostnames allows DNS SANs to authenticate IP endpoints | medium | 26, 24, 22 |
27+
| 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 |
28+
| CVE-2026-106070 | Node.js ZipFile returns attacker-controlled prefix bytes after failed ZIP header validation | low | 26 |
29+
| CVE-2026-106071 | Permission Model filesystem over-grant via wildcard radix lookup | low | 26 |
30+
| CVE-2026-106073 | Permission Model bypass with fs.realpathSync | low | 26, 24, 22 |
31+
32+
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.
33+
34+
### The changelogs
35+
36+
From `doc/changelogs/CHANGELOG_V22.md`, `CHANGELOG_V24.md` and `CHANGELOG_V26.md` in [nodejs/node](https://github.com/nodejs/node):
37+
38+
- All three list "(CVE-2026-111497) deps: brotli: cherry-pick 0ed4268a4401abcb44720963" and "(CVE-2026-111497) src: expose `BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH`".
39+
- All three list "crypto: update root certificates to NSS 3.131.1" and "tls: reject chains containing blocked public keys".
40+
- 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).
41+
- 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.
42+
- "(CVE-2026-105482) dns: validate resolve inputs for null bytes" in all three.
43+
- 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.
44+
- 26.12.1 also reverts "module: synchronously load most ES modules" (commit e5cae10148).
45+
46+
### CVE records
47+
48+
- At 2026-10-22T06:01Z the CVE Services API returned `"state":"RESERVED"` for all ten IDs (`https://cveawg.mitre.org/api/cve-id/<id>`), and `CVE_RECORD_DNE` for 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.
49+
50+
### Brotli upstream
51+
52+
- The cherry-picked commit is [google/brotli 0ed4268](https://github.com/google/brotli/commit/0ed4268a4401abcb4472096340477cf4af7d1a2b), 2026-02-27, "Detect and report invalid stream earlier.", changing `c/dec/decode.c` and `c/include/brotli/decode.h`.
53+
- Brotli published [v1.2.1](https://github.com/google/brotli/releases/tag/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".
54+
- Debian builds Node against the system Brotli: `debian/rules` of nodejs 20.19.2+dfsg-1+deb13u2 (trixie) passes `--shared-brotli` (and `--shared`, `--shared-zlib`, `--shared-openssl`), and `debian/control` build-depends on `libbrotli-dev` ([sources.debian.org](https://sources.debian.org/src/nodejs/20.19.2+dfsg-1+deb13u2/debian/rules/)). For such builds the Brotli fix comes from the distribution's brotli package.
55+
- 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.
56+
57+
## Who is exposed: code paths into the Brotli decoder
58+
59+
- `fetch()` in Node is undici: "The implementation is based upon undici" (`doc/api/globals.md` at v24.22.1).
60+
- undici's fetch decodes `Content-Encoding: br` responses with `zlib.createBrotliDecompress` (`require('node:zlib')`): [deps/undici/src/lib/web/fetch/index.js at v24.22.1](https://github.com/nodejs/node/blob/v24.22.1/deps/undici/src/lib/web/fetch/index.js#L2256-L2260), and the same code at v22.23.4 (line 2164) and v26.12.1 (line 2322).
61+
- The decoders are chosen from the response's `Content-Encoding` header only. The request's `Accept-Encoding` (`br, gzip, deflate` for https, `gzip, deflate` for 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.
62+
- axios in Node decompresses `br` responses unless `decompress: false`: [lib/adapters/http.js at v1.20.0](https://github.com/axios/axios/blob/v1.20.0/lib/adapters/http.js#L1179-L1209).
63+
- body-parser 2.0.0 (2024-09-10) "Add brotli support" ([HISTORY.md](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)); `inflate` "Defaults to `true`" (README); [lib/read.js at v2.3.0](https://github.com/expressjs/body-parser/blob/v2.3.0/lib/read.js#L190-L224) decompresses `br` request bodies and returns 415 when `inflate: false`. Express 5.3.0 depends on `body-parser ^2.3.0` and exports `express.json = bodyParser.json`. body-parser 1.20.8 (Express 4) handles only deflate, gzip and identity.
64+
- The decoder is compiled into the `node` binary (`deps/brotli`), so updating an npm package such as `undici` does not change it.
65+
66+
## Release lines and images
67+
68+
- Node release schedule ([nodejs/Release schedule.json](https://github.com/nodejs/Release/blob/main/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.
69+
- docker-library/official-images commit 74ff70c42b, "Node: feat: Node.js 22.23.4, 24.22.1, 26.12.1", at 2026-10-21T10:48:56Z. The `library/node` file no longer lists tags for 20 or 25.
70+
- Our scan of 24 official `node` tags for linux/amd64 and linux/arm64, from registry metadata only: `results/registry-20261022T062338Z.json`, summary in `results/registry-20261022T062338Z-summary.txt`. Docker Hub's per-image `last_pushed` is metadata, not proof of when a tag first served an image.
71+
72+
## pm2 and nvm behavior
73+
74+
- pm2 docs, [Startup](https://pm2.keymetrics.io/docs/usage/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 unstartup` then `pm2 startup`; and "save the app list so it will respawn after reboot: pm2 save".
75+
- pm2 docs, [Updating PM2](https://pm2.keymetrics.io/docs/usage/update-pm2/): "update the in-memory PM2 daemon via command: pm2 update".
76+
- pm2 6.0.14 CLI: `-a --update-env` "force an update of the environment with restart/reload" (`lib/binaries/CLI.js` line 49).
77+
- pm2 6.0.14 `lib/Common.js` `resolveInterpreter`: a `.js` script gets the interpreter `node` (from `lib/API/interpreter.json`), resolved through the app's environment when spawned; a script without a known extension that is not a binary gets `process.execPath`, the absolute path of the Node running the pm2 CLI. `lib/God/ForkMode.js` spawns the interpreter with the app's saved environment.
78+
- pm2 cluster mode: `lib/God/ClusterMode.js` calls `cluster.fork()`, and Node's cluster primary forks workers with `child_process.fork()` (`lib/internal/cluster/primary.js` at v24.22.1), which defaults `execPath` to `process.execPath` (`lib/child_process.js` line 144). So cluster-mode workers run on the daemon's Node whatever the entry's interpreter or PATH says. The lab shows it: the `clustered` app stayed on v24.14.0 after `restart --update-env` and moved only after `pm2 update`.
79+
- nvm README: "New shells will start with the default version of node (e.g., `nvm alias default`)", and `nvm install --reinstall-packages-from=...` to move global npm packages to the new version.
80+
- `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: bare `node`, `npm start`, an absolute `--interpreter` and cluster mode.
81+
82+
## Our own runs
83+
84+
- `examples/run-20261022T062905Z.txt` and `.json`: `node_runtime_audit.py --redact` on the Raspberry Pi we build on, 2026-10-22T06:29:05Z, as a normal user.
85+
- The production upgrade described in the post (six Node apps under pm2, installed with nvm) was done by the team on 21 October. Its details come from that work and are generalized; the lab reproduces the pm2 behaviors on a separate machine.

0 commit comments

Comments
 (0)