# Node.js Security Update October 2026: Find and Patch Every Node.js on Your Server

Source: https://srvscripts.com/guides/nodejs-security-update-october-2026/
Updated: 2026-10-22
Publisher: srvScripts (https://srvscripts.com/)

Node.js released security updates on 21 October 2026 for its 22, 24 and 26 release lines. The most serious flaw, CVE-2026-111497, is an out-of-bounds write in the Brotli decoder that the Node.js project says can allow remote code execution. The fixed versions are **22.23.4**, **24.22.1** and **26.12.1**; Node.js 20 and older are end of life and get no fix.

**Short answer:** find every Node.js on the server, not just the one `node -v` shows: system packages, NodeSource, nvm and other version managers in each user’s home, cPanel and CloudLinux packages, and Docker images. Update each one to a fixed version (or move off Node.js 20 and older), restart every app that uses it, then check again that no running process still uses an old binary. The read-only script below does the finding and the final check.

**Scope and evidence:** facts are from the Node.js security release post, the CloudLinux status post and the Node.js download index, checked on 22 October 2026. The inventory script (version 2, revised on 23 October 2026 after an independent review) was tested with fixtures on a test machine (33 of 33 checks passed), read-only on our FreePBX lab (Debian 12) and on our production server, and on our DirectAdmin lab through a full nvm update and a PM2 upgrade with its boot service. The NodeSource, cPanel, CloudLinux, Plesk, Docker and n/fnm/Volta/asdf update steps follow the vendors’ documentation and were not run for this guide.

In short: Find every Node.js on the server, update each to 22.23.4, 24.22.1 or 26.12.1 (or move off 20 and older), restart every app, then check that no process still runs an old binary.

## What the 21 October 2026 releases fix

| Release line | Fixed in | Status |
| --- | --- | --- |
| Node.js 26 | 26.12.1 | Supported |
| Node.js 24 | 24.22.1 | Supported |
| Node.js 22 | 22.23.4 | Supported |
| Node.js 20, 18 and older; odd lines such as 23 and 25 | no upstream fix | End of life: the project says end-of-life versions are always affected; move to 22 or newer |

| CVE | Severity | Lines | What it is (per the Node.js post) |
| --- | --- | --- | --- |
| CVE-2026-111497 | High | 26, 24, 22 | Out-of-bounds write in the Brotli decoder; can allow remote code execution |
| CVE-2026-105482 | High | 26, 24, 22 | dns.resolve truncates names that contain a NUL character |
| CVE-2026-105483 | High | 26, 24 | TLS hostname check bypassed when an IP address is written with fullwidth digits |
| CVE-2026-106069 | High | 26, 24, 22 | TLS hostname check bypassed when the host loosely resolves to an IP address |
| CVE-2026-105481, -106072, -106074 | Medium | 26, 24, 22 | Keep-alive response confusion; non-canonical IPv4 names matching DNS certificate names; repeated random values in startup snapshots and single-executable apps |
| CVE-2026-106070, -106071, -106073 | Low | mostly 26 | ZipFile prefix bytes; Permission Model over-grant and bypass |

The releases also update c-ares, the CA certificate list, OpenSSL (24 and 22), SQLite (22) and Undici. The Node.js post does not say which inputs or APIs trigger the Brotli flaw, so treat every internet-facing Node.js app as in scope rather than guessing.

## Why node -v is not enough

`node -v` reports only the first Node.js in your shell’s `PATH`. A typical server can also have a NodeSource or distribution package, a separate copy for each user installed with nvm, n, fnm, Volta or asdf, cPanel `ea-nodejs` and CloudLinux `alt-nodejs` packages for the Node.js Selector, Plesk’s Node.js versions, and Node.js inside Docker images. Automatic OS updates cover only some of these, and none of them touch software inside containers.

Running apps are the second gap: a process keeps using the binary it started with. After an update, an app that was not restarted still runs the old Node.js, and if the old version was removed, it runs a deleted file that `node -v` will never show.

## Find every Node.js on the server

This script lists every Node.js it finds in the usual places, every running Node.js process and every running container, and marks each one OK, UPDATE, EOL, RESTART or UNKNOWN. It is read-only, but it does run `<binary> -v` to read a version, with a 5-second timeout. As root it does that only for binaries whose whole path (the file and every folder above it) is owned by root and not writable by anyone else. A binary inside a user’s home is run as that user, and anything else is reported as UNKNOWN without being run, so a file another account can swap is never executed as root.

Anything it cannot check is UNKNOWN, never OK: pre-release builds such as `v22.23.4-rc.1`, release lines the advisory does not cover, binaries it may not run, and containers. It only runs a shell inside containers when you set `DOCKER_EXEC=1`, with a 10-second limit, and separates “no node on the container’s PATH” from “could not look”. It does not print process command lines, because they can contain passwords and tokens; `SHOW_ARGS=1` adds them with a warning. For distribution packages it also reports whether Node.js uses the system Brotli library.

```
#!/bin/bash
# node-inventory.sh v2 - read-only check of every Node.js on a Linux server against the 21 October 2026
# security releases (fixed: 22.23.4, 24.22.1, 26.12.1). It changes nothing.
#   bash node-inventory.sh                  installed copies, running processes, containers
#   DOCKER_EXEC=1 bash node-inventory.sh    also runs a shell inside each running container to ask "node -v"
#   SHOW_ARGS=1 bash node-inventory.sh      also prints process command lines (may contain passwords or tokens)
# To read a version it runs " -v". As root it does that only for binaries whose whole path is owned by
# root and not writable by anyone else; a binary inside a user's home is run as that user; anything else is
# reported UNKNOWN and not run. Exit code: 0 = nothing to do, 1 = update or restart needed, 2 = something unknown.
set -u
ROOT=${ROOT:-}; PROC=${PROC:-/proc}           # test prefixes; leave empty on a real server
T=5; CT=10                                      # timeouts in seconds: node -v, container commands
declare -A FIXED=( [22]=22.23.4 [24]=24.22.1 [26]=26.12.1 ); HIGHEST=26
need=0; unknown=0

ge() { [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]; }        # $1 >= $2
trusted_chain() { # the file and every parent directory owned by root and not writable by group or others
	local p=$1 u m
	while :; do
		read -r u m < /dev/null) || return 1
		[ "$u" = 0 ] && (( (8#$m & 8#022) == 0 )) || return 1
		[ "$p" = / ] && return 0
		p=$(dirname -- "$p")
	done
}
home_account() { # account (uid >= 1000) whose home directory contains $1 and owns that directory
	local p=$1 user uid home best="" blen=0 o
	while IFS=: read -r user _ uid _ _ home _; do
		[ "$uid" -ge 1000 ] 2>/dev/null && [ "$uid" -ne 65534 ] && [ -n "$home" ] && [ "$home" != / ] || continue
		case "$p" in "$home"/*) ;; *) continue ;; esac
		o=$(stat -c %u -- "$home" 2>/dev/null) || continue; [ "$o" = "$uid" ] || continue
		[ ${#home} -gt $blen ] && { best=$user; blen=${#home}; }
	done < /dev/null) || return 4
	elif trusted_chain "$b"; then out=$(timeout $T "$b" -v 2>/dev/null) || return 4
	elif a=$(home_account "$b"); then out=$(runuser -u "$a" -- timeout $T "$b" -v 2>/dev/null) || return 4
	else return 3; fi
	printf '%s' "$out" | head -1
}
pkg_of() { local f=$1 o
	if command -v rpm >/dev/null 2>&1 && o=$(rpm -qf --qf '%{NAME}\n' -- "$f" 2>/dev/null); then printf '%s\n' "$o" | head -1; return; fi
	if command -v dpkg >/dev/null 2>&1 && o=$(dpkg -S -- "$f" 2>/dev/null); then printf '%s\n' "$o" | head -1 | cut -d: -f1; fi; }
pkg_ver() { rpm -q --qf '%{VERSION}-%{RELEASE}' "$1" 2>/dev/null || dpkg-query -W -f='${Version}' "$1" 2>/dev/null; }
brotli_note() { # distribution builds can use the system Brotli library instead of the copy bundled in Node.js
	command -v readelf >/dev/null 2>&1 || return 0
	local libs l lp p
	libs=$(readelf -d -- "$1" 2>/dev/null | sed -n 's/.*Shared library: \[\(.*\)\]/\1/p')
	for l in $libs; do case $l in libnode.so*) lp=$(ldconfig -p 2>/dev/null | awk -v l="$l" '$1==l {print $NF; exit}'); [ -n "$lp" ] && libs="$libs $(readelf -d -- "$lp" 2>/dev/null | sed -n 's/.*Shared library: \[\(.*\)\]/\1/p')" ;; esac; done
	libs=$(printf '%s\n' $libs | tr '\n' ' ')
	case " $libs " in *" libbrotlidec.so"*) ;; *) return 0 ;; esac
	lp=$(ldconfig -p 2>/dev/null | awk '$1 ~ /^libbrotlidec\.so/ {print $NF; exit}'); [ -n "$lp" ] || return 0
	p=$(pkg_of "$(readlink -f -- "$lp")"); echo "uses the system Brotli library${p:+ from package $p $(pkg_ver "$p")}: check that package's security status too"
}
verdict() { # $1 = output of node -v
	local raw=${1#v} M
	if [[ ! $raw =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
		if [[ $raw =~ ^[0-9]+\.[0-9]+\.[0-9]+[-+.~]?(rc|pre|dev|nightly|alpha|beta|canary) ]]; then echo "UNKNOWN pre-release build $1: not a stable release, check it by hand"
		else echo "UNKNOWN version not readable (${1:0:40})"; fi; return
	fi
	M=${BASH_REMATCH[1]}
	if [ -n "${FIXED[$M]:-}" ]; then
		if ge "$raw" "${FIXED[$M]}"; then echo "OK      $raw is at or above ${FIXED[$M]}"; else echo "UPDATE  $raw is older than ${FIXED[$M]}"; fi
	elif [ "$M" -lt 22 ]; then echo "EOL     $raw: Node.js $M is end of life, no upstream fix; move to 22 or newer"
	elif [ "$M" -lt $HIGHEST ] && [ $((M % 2)) -eq 1 ]; then echo "EOL     $raw: Node.js $M is no longer supported; move to 22, 24 or 26"
	else echo "UNKNOWN $raw: Node.js $M is not covered by the 21 October 2026 advisory, check its own release notes"; fi
}
show() { # $1 kind, $2 subject, $3 verdict, $4 note
	case "$3" in UPDATE*|EOL*|RESTART*) need=1 ;; UNKNOWN*) unknown=1 ;; esac
	printf '%-9s %s\n          %s%s\n' "$1" "$2" "$3" "${4:+ · $4}"
}
explain_rc() { case $1 in 3) echo "UNKNOWN not run: not in a root-only location or inside a user's home" ;; *) echo "UNKNOWN could not read the version (failed or timed out)" ;; esac; }

echo "== Installed copies"
declare -A seen; found=0
set -f   # keep the patterns literal here; they are expanded below, under $ROOT
globs=( /usr/bin/node /usr/bin/nodejs /usr/local/bin/node /usr/local/n/versions/node/*/bin/node /opt/nodejs*/bin/node /opt/node*/bin/node
	/opt/cpanel/ea-nodejs*/bin/node /opt/alt/alt-nodejs*/root/usr/bin/node /opt/plesk/node/*/bin/node
	/root/.nvm/versions/node/*/bin/node /home/*/.nvm/versions/node/*/bin/node /root/.volta/tools/image/node/*/bin/node /home/*/.volta/tools/image/node/*/bin/node
	/root/.local/share/fnm/node-versions/*/installation/bin/node /home/*/.local/share/fnm/node-versions/*/installation/bin/node
	/root/.asdf/installs/nodejs/*/bin/node /home/*/.asdf/installs/nodejs/*/bin/node )
set +f
for g in "${globs[@]}"; do
	for f in $ROOT$g; do
		[ -e "$f" ] || continue
		r=$(readlink -f -- "$f"); [ -n "${seen[$r]:-}" ] && continue; seen[$r]=1
		[ -f "$r" ] && [ -x "$r" ] || continue
		found=1; owner=$(stat -c %U -- "$r")
		ver=$(version_of "$r"); vr=$?
		if [ $vr -ne 0 ]; then show COPY "$r" "$(explain_rc $vr)" "owner $owner"; continue; fi
		p=$(pkg_of "$r"); note="owner $owner"
		if [ -n "$p" ]; then
			note="package $p $(pkg_ver "$p"), $note"
			b=$(brotli_note "$r"); [ -n "$b" ] && note="$note; $b"
		fi
		v=$(verdict "$ver")
		[ -n "$p" ] && case "$v" in UPDATE*|EOL*) note="$note; a distribution may backport fixes, check its advisory" ;; esac
		show COPY "$r" "$v" "$note"
	done
done
[ $found -eq 0 ] && echo "          no Node.js binary found in the usual places"

echo "== Running processes"
[ -n "${SHOW_ARGS:-}" ] && echo "          WARNING: command lines below can contain passwords or tokens; do not share this output"
any=0
for d in "$PROC"/[0-9]*; do
	exe=$(readlink -- "$d/exe" 2>/dev/null) || continue
	case "$(basename -- "${exe% (deleted)}")" in node|nodejs) ;; *) continue ;; esac
	any=1; pid=${d##*/}; u=$(stat -c %U -- "$d" 2>/dev/null)
	tag=""; grep -qa 'PM2 v' "$d/cmdline" 2>/dev/null && tag=" [PM2 daemon]"
	subj="pid $pid, account $u, ${exe}${tag}"
	[ -n "${SHOW_ARGS:-}" ] && subj="$subj, args: $(tr '\0' ' ' < "$d/cmdline" 2>/dev/null)"
	if [[ "$exe" == *" (deleted)" ]]; then show PROCESS "$subj" "RESTART its binary was replaced or removed after it started: it still runs the old Node.js"; continue; fi
	ver=$(version_of "$exe"); vr=$?
	if [ $vr -ne 0 ]; then show PROCESS "$subj" "$(explain_rc $vr)"; continue; fi
	show PROCESS "$subj" "$(verdict "$ver")"
done
[ $any -eq 0 ] && echo "          no running Node.js process"

echo "== Containers"
rts=0
for rt in docker podman; do
	command -v $rt >/dev/null 2>&1 || continue; rts=1
	if ! list=$(timeout $CT $rt ps --format '{{.ID}} {{.Image}} {{.Names}}' 2>/dev/null); then show CONTAINER "$rt" "UNKNOWN containers could not be listed (service not running, no permission or timeout)"; continue; fi
	[ -z "$list" ] && { echo "          $rt: no running containers"; continue; }
	while read -r id img name; do
		if [ -z "${DOCKER_EXEC:-}" ]; then show CONTAINER "$name ($img)" "UNKNOWN not checked inside: run with DOCKER_EXEC=1 or \"$rt exec $name node -v\""; continue; fi
		out=$(timeout $CT $rt exec "$id" sh -c 'command -v node >/dev/null 2>&1 || exit 64; node -v' 2>/dev/null); rc=$?
		case $rc in
			0)   show CONTAINER "$name ($img)" "$(verdict "$(printf '%s' "$out" | head -1)")" ;;
			64)  printf '%-9s %s\n          NONE    no node on the container'"'"'s PATH\n' CONTAINER "$name ($img)" ;;
			124) show CONTAINER "$name ($img)" "UNKNOWN timed out after ${CT}s" ;;
			*)   show CONTAINER "$name ($img)" "UNKNOWN could not run a shell in the container (exit $rc)" ;;
		esac
	done
