Skip to content

Commit d00f50a

Browse files
committed
Never run found binaries by default; sanitize output
The version now comes from the file itself (embedded headers URL or node_version.h). --exec-version is opt-in and runs a binary only through a pinned O_PATH descriptor when root or the caller owns it and every directory above it. Control characters from command lines, file and container names are escaped as \xNN in text and JSON.
1 parent 5466bb3 commit d00f50a

7 files changed

Lines changed: 314 additions & 105 deletions

File tree

‎README.md‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ sudo python3 node_runtime_audit.py # or without sudo: your own processes
77

88
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).
99

10-
It is one file of standard-library Python (3.8+, Linux). It only reads: it never restarts, edits or upgrades anything.
10+
It is one file of standard-library Python (3.8+, Linux). It only reads: it never restarts, edits or upgrades anything, and by default it never runs a binary it finds (see Safety below).
1111

1212
Written for the DevOps Daily post: https://devops-daily.com/posts/node-october-2026-security-release-find-every-node
1313

1414
## What it checks
1515

1616
| Where | How |
1717
| --- | --- |
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. |
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), or `include/node/node_version.h` of the same install. A version from a directory name alone is shown as a hint and stays unknown. |
1919
| 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. |
2020
| pm2, saved | `dump.pm2`, the list `pm2 resurrect` and a reboot start. |
2121
| Version managers | nvm, fnm, volta, asdf, mise, nodenv and n installs, and the default alias. |
@@ -49,12 +49,22 @@ for h in web1 web2; do ssh "$h" 'sudo python3 - --json' < node_runtime_audit.py
4949
## Results
5050

5151
- `results/pm2-lab-20261022T063002Z.txt`: pm2 6.0.14 with Node v24.14.0 and v24.22.1.
52-
- `examples/run-20261022T062905Z.txt`: the script on our Raspberry Pi.
52+
- `examples/run-20261022T063927Z.txt`: the script on our Raspberry Pi.
5353
- `results/registry-20261022T062338Z.json`: 24 official tags, amd64 and arm64.
5454
- `results/pgrep-vs-proc-20261022T063334Z.txt`: `pgrep -c node` returns 0 on the Pi while five processes run a `node` binary.
5555

5656
Facts and their sources are in [SOURCES.md](SOURCES.md).
5757

58+
## Safety
59+
60+
The script is meant to run as root on servers where other users run Node, so it treats everything it finds as untrusted:
61+
62+
- **It does not execute the binaries it finds.** The version comes from the file itself: the `node-vX.Y.Z-headers.tar.gz` URL that official builds compile in, read through `/proc/<pid>/exe`, or `include/node/node_version.h` of the install. Running `<path> --version` as root on a path that another user controls would run their code as root.
63+
- **`--exec-version` is opt-in** and only runs a binary when root (or you, when you are not root) owns the file and every directory from `/` down to it, and none of them is writable by group or others. As root it refuses any binary not owned by root. The file is opened with `O_PATH | O_NOFOLLOW`, checked with `fstat` against the inode the process runs, and executed through `/proc/self/fd/N`, so it cannot be swapped between the check and the exec. If the result disagrees with the embedded version, the answer is unknown.
64+
- **Output is sanitized.** Command lines, process names, file names and container names and images come from other users and containers. Every control character (`0x00` to `0x1f`, `0x7f` and `0x80` to `0x9f`) is printed as `\xNN`, in text and in JSON, so nothing can send escape sequences to your terminal.
65+
- **Secrets stay out.** It prints only the first two arguments of a command line and drops any that contain `=`. From pm2 environments, crontabs and units it prints only the Node paths, never whole lines or other variables.
66+
- It only runs `pm2 jlist` for your own `PM2_HOME` and only when that daemon is already running, because `pm2 jlist` starts a daemon otherwise and, as root, would leave root-owned files in another user's `PM2_HOME`.
67+
5868
## Limits
5969

6070
- Linux only (`/proc`). Not root: other users' processes and crontabs are not visible, and the script says so.

‎SOURCES.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,5 @@ From `doc/changelogs/CHANGELOG_V22.md`, `CHANGELOG_V24.md` and `CHANGELOG_V26.md
8383

8484
- `results/pgrep-vs-proc-20261022T063334Z.txt`: on the Pi, `pgrep -c node` and `pgrep -c -x node` both returned 0 while five processes ran a `node` binary, named `PM2 v6.0.14: Go`, `MainThread` and `next-server (v1` in `/proc/<pid>/comm`.
8585

86-
- `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.
86+
- `examples/run-20261022T063927Z.txt` and `.json`: `node_runtime_audit.py --redact` on the Raspberry Pi we build on, 2026-10-22T06:39:27Z, as a normal user.
8787
- 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.
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"deleted": false,
1818
"container": false,
1919
"version": "v24.14.0",
20-
"method": "embedded+--version",
20+
"method": "embedded",
2121
"status": "vulnerable",
2222
"reason": "v24.14.0 < v24.22.1",
2323
"notes": [],
@@ -34,7 +34,7 @@
3434
"deleted": false,
3535
"container": false,
3636
"version": "v24.14.0",
37-
"method": "embedded+--version",
37+
"method": "embedded",
3838
"status": "vulnerable",
3939
"reason": "v24.14.0 < v24.22.1",
4040
"notes": [],
@@ -53,7 +53,7 @@
5353
"deleted": false,
5454
"container": false,
5555
"version": "v24.14.0",
56-
"method": "embedded+--version",
56+
"method": "embedded",
5757
"status": "vulnerable",
5858
"reason": "v24.14.0 < v24.22.1",
5959
"notes": [],
@@ -73,7 +73,7 @@
7373
"deleted": false,
7474
"container": false,
7575
"version": "v24.14.0",
76-
"method": "embedded+--version",
76+
"method": "embedded",
7777
"status": "vulnerable",
7878
"reason": "v24.14.0 < v24.22.1",
7979
"notes": [],
@@ -100,7 +100,7 @@
100100
"deleted": false,
101101
"container": false,
102102
"version": "v24.14.0",
103-
"method": "embedded+--version",
103+
"method": "embedded",
104104
"status": "vulnerable",
105105
"reason": "v24.14.0 < v24.22.1",
106106
"notes": [],
@@ -114,6 +114,7 @@
114114
"name": "app-1",
115115
"status_pm2": "online",
116116
"pid": 1883370,
117+
"cluster": false,
117118
"interpreter": "none",
118119
"script": "/home/USER/.bun/bin/bun",
119120
"findings": [
@@ -136,7 +137,7 @@
136137
"deleted": false,
137138
"container": false,
138139
"version": "v24.14.0",
139-
"method": "embedded+--version",
140+
"method": "embedded",
140141
"status": "vulnerable",
141142
"reason": "v24.14.0 < v24.22.1",
142143
"notes": [],
@@ -155,7 +156,7 @@
155156
"deleted": false,
156157
"container": false,
157158
"version": "v24.14.0",
158-
"method": "embedded+--version",
159+
"method": "embedded",
159160
"status": "vulnerable",
160161
"reason": "v24.14.0 < v24.22.1",
161162
"notes": [],
@@ -175,7 +176,7 @@
175176
"deleted": false,
176177
"container": false,
177178
"version": "v24.14.0",
178-
"method": "embedded+--version",
179+
"method": "embedded",
179180
"status": "vulnerable",
180181
"reason": "v24.14.0 < v24.22.1",
181182
"notes": [],
@@ -200,6 +201,7 @@
200201
"name": "app-1",
201202
"status_pm2": "online",
202203
"pid": null,
204+
"cluster": false,
203205
"interpreter": "none",
204206
"script": "/home/USER/.bun/bin/bun",
205207
"findings": [
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 2026-10-22T06:29:05Z Raspberry Pi (Debian 12, aarch64), Node via fnm, pm2 6.0.14, run as a normal user
1+
# 2026-10-22T06:39:27Z Raspberry Pi (Debian 12, aarch64), Node via fnm, pm2 6.0.14, run as a normal user
22
$ python3 node_runtime_audit.py --redact
33
node-runtime-audit: Node.js security release of 2026-10-21, fixed releases v22.23.4, v24.22.1, v26.12.1
44
lines without a fix (end-of-life) count as vulnerable
@@ -21,7 +21,7 @@ Running Node processes (real binary from /proc/<pid>/exe)
2121
cmd: node /home/USER/projects/app-1/.next/dev/build/postcss.js 38963
2222
parents: node(1883390) <- node(1883371) <- bun(1883370) <- node(1883303)
2323
why: v24.14.0 < v24.22.1
24-
177 other processes could not be inspected (run as root to see every user)
24+
178 other processes could not be inspected (run as root to see every user)
2525

2626
pm2 live list (pm2 jlist, PM2_HOME=/home/USER/.pm2)
2727
[vulnerable] daemon pid 1883303 v24.14.0 /home/USER/.local/share/fnm/node-versions/v24.14.0/installation/bin/node

0 commit comments

Comments
 (0)