Node.js plans to ship security fixes on or shortly after tomorrow, and nobody outside the project knows yet what they fix.
That is the whole announcement. The Node.js project says new versions of three release lines will come out on or shortly after Wednesday, November 11, 2026. The highest severity is HIGH in each of the three lines. The sentence that should list the issues ends with a colon and then nothing. I read the page live at about 08:00 Jakarta time this morning, and the list was still empty.
So here is the question you will get in your team chat on Wednesday: “Are we affected?” The honest first answer is not yes or no. It is a question back: “Which Node?”
If you cannot answer that in one minute, today is the day to fix it. You do not need the CVE details for this part. You need a list of the places where Node runs and a name next to each one. When the details land, you read them against a list instead of against your memory.
What Node.js actually said
Here is everything the announcement page says, and I will not add to it.
The project will release new versions of the 26.x, 24.x and 22.x lines on or shortly after Wednesday, November 11, 2026. The highest severity fixed is HIGH, and it says so separately for each of the three lines. It also says: “End-of-Life versions are always affected when a security release occurs.” There is no CVE list, no description of the bug and no hour of the day.
A “release line” is the number before the first dot. Node 22 is one line, and 22.23.4 is one version inside it. Node.js publishes a schedule that says which lines are supported. I read it today from the project’s nodejs/Release repository:
| Line | Status on Nov 10, 2026 | On Wednesday’s list? | Support ends |
|---|---|---|---|
| 26 (Lithium) | LTS since 2026-10-28 | Yes | 2029-04-30 |
| 24 (Krypton) | Maintenance LTS since 2026-10-20 | Yes | 2028-04-30 |
| 22 (Jod) | Maintenance LTS | Yes | 2027-04-30 |
| 25 | End-of-Life | No, but always affected | ended 2026-06-01 |
| 20 (Iron) | End-of-Life | No, but always affected | ended 2026-04-30 |
LTS means long-term support, the lines meant to run in production. Maintenance LTS is the later stage where a line gets fixes but no new features. End-of-Life means the project no longer releases for it. If any of your servers still run Node 20 or Node 25, the announcement’s own sentence applies to them. Wednesday’s list does not name them, and they are affected anyway.

Five places a Node version hides
Ask a team “which Node do you run?” and you usually get one answer. The honest answer is often five.
| Place | How to read it | The trap |
|---|---|---|
| Your laptop | node -v | It is the one you tested on, so it feels like the answer |
| The CI runner | the setup step in the workflow file | The file says what you asked for, not what the runner had cached |
| The Docker image | the FROM node: line | A tag like lts moves without a commit |
| The build platform | its dashboard or a pin file | You never installed it, so you never looked |
| The machine your coding agent runs on | node -v in that shell | Nobody put it on any list |
The last row is the one I want you to look at. A coding agent runs shell commands, and shell commands call whatever node is on the path. Nobody chose that Node. It came with the host.
I checked mine this morning. The machine that runs my own agent jobs reports v26.12.1. The 26 line is on Wednesday’s list, and the newest 26 on nodejs.org is 26.13.0, released on 2026-10-28, so this host is one minor version behind. I had not written that down anywhere until now.
The repository for this very site has no .nvmrc and no package.json. Hugo builds it, and no file in the repository asks for Node. That is also an answer, and it is worth writing down: “not here” is a finding. Silence is not.

Step one: read the files
Files tell you what a human asked for. They are the cheapest thing to scan, so I scan them first. This script walks a repository and lists every place that names a Node version: .nvmrc, .node-version, package.json, Dockerfile, compose files and GitHub workflows. For each hit it says which release line the value belongs to and whether that line is on Wednesday’s list.
It uses only the Python standard library. It reads nothing outside the folder you give it, and it fails closed: a folder it cannot read exits with code 3 instead of printing an empty, calm report.
1#!/usr/bin/env python3
2"""List every place a repo names a Node.js version. Usage: find_node_pins.py PATH
3Exit 3 = PATH unreadable. Finding nothing is NOT a clean result."""
4import json
5import os
6import re
7import sys
8
9# Release lines, checked against nodejs/Release schedule.json on 2026-11-10.
10# (status, on the Nov 11 security release list?)
11LINES = {
12 20: ("EOL since 2026-04-30", False),
13 22: ("Maintenance LTS, EOL 2027-04-30", True),
14 24: ("Maintenance LTS, EOL 2028-04-30", True),
15 25: ("EOL since 2026-06-01", False),
16 26: ("LTS, EOL 2029-04-30", True),
17 27: ("alpha, not released", False),
18}
19CODENAMES = {"iron": 20, "jod": 22, "krypton": 24, "lithium": 26}
20FLOATING = ("lts", "latest", "current", "node", "stable")
21SKIP_DIRS = {".git", "node_modules", "vendor", "dist", "public", ".venv"}
22
23
24def classify(spec):
25 raw = spec.strip().strip("'\"")
26 low = raw.lower()
27 if not raw:
28 return "empty", "read by a human"
29 if "$" in raw or "{{" in raw:
30 return "indirect", "value set elsewhere: follow it"
31 if re.search(r"[<>^~|\s]", raw) and not low.startswith(("lts/", "lts-")):
32 return "range", "range, not a pin: check what really runs"
33 if low.startswith(FLOATING):
34 return "floating", "moves when someone re-pulls or rebuilds"
35 m = re.match(r"^v?(\d+)", low)
36 if m:
37 major = int(m.group(1))
38 else:
39 major = next((v for k, v in CODENAMES.items() if low.startswith(k)), None)
40 if major is None:
41 return "unknown", "read by a human"
42 status, listed = LINES.get(major, ("older than 20", False))
43 exact = bool(re.match(r"^v?\d+\.\d+\.\d+", low))
44 kind = "exact patch" if exact else "line only"
45 verdict = (
46 "on Wednesday's list" if listed else "EOL: always affected, not on the list"
47 )
48 return f"{kind} (node {major}, {status})", verdict
49
50
51def read_lines(path):
52 with open(path, encoding="utf-8", errors="replace") as fh:
53 return fh.read().splitlines()
54
55
56def scan_file(path, rel):
57 name = os.path.basename(path)
58 out = []
59 lines = read_lines(path)
60 if name in (".nvmrc", ".node-version"):
61 for i, ln in enumerate(lines, 1):
62 if ln.strip():
63 out.append((rel, i, name, ln.strip()))
64 break
65 elif name == "package.json":
66 try:
67 data = json.loads("\n".join(lines))
68 except ValueError:
69 out.append((rel, 1, "package.json", "UNREADABLE JSON"))
70 return out
71 eng = (data.get("engines") or {}).get("node")
72 if eng:
73 out.append((rel, 1, "engines.node", eng))
74 vol = (data.get("volta") or {}).get("node")
75 if vol:
76 out.append((rel, 1, "volta.node", vol))
77 elif name.lower().startswith("dockerfile") or name.lower().endswith(".dockerfile"):
78 pat = re.compile(r"^\s*FROM\s+(?:--platform=\S+\s+)?node:(\S+)", re.I)
79 for i, ln in enumerate(lines, 1):
80 m = pat.match(ln)
81 if m:
82 out.append((rel, i, "FROM node:", m.group(1)))
83 elif re.search(r"(^|/)(docker-)?compose[^/]*\.ya?ml$", rel):
84 pat = re.compile(r"^\s*image:\s*['\"]?node:([^\s'\"#]+)")
85 for i, ln in enumerate(lines, 1):
86 m = pat.match(ln)
87 if m:
88 out.append((rel, i, "compose image: node:", m.group(1)))
89 elif ".github/workflows/" in rel and name.endswith((".yml", ".yaml")):
90 pat = re.compile(r"^\s*-?\s*(node-version(?:-file)?):\s*(.+?)\s*(?:#.*)?$")
91 for i, ln in enumerate(lines, 1):
92 m = pat.match(ln)
93 if m:
94 out.append((rel, i, m.group(1), m.group(2)))
95 return out
96
97
98def main(argv):
99 if len(argv) != 2 or not os.path.isdir(argv[1]) or not os.access(argv[1], os.R_OK):
100 print("ERROR: give a readable repo folder", file=sys.stderr)
101 return 3
102 root = argv[1]
103 found = []
104 for base, dirs, files in os.walk(root):
105 dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
106 for f in files:
107 path = os.path.join(base, f)
108 rel = os.path.relpath(path, root)
109 try:
110 found.extend(scan_file(path, rel))
111 except OSError as exc:
112 print(f"ERROR: cannot read {rel}: {exc}", file=sys.stderr)
113 return 3
114 if not found:
115 print("NO NODE PINS FOUND. This is not 'safe'. The version comes from")
116 print("somewhere this scan cannot see: host default, base image, laptop.")
117 return 0
118 print("| where | what | value | what it is | Wednesday |")
119 print("|---|---|---|---|---|")
120 for rel, line, what, value in sorted(found):
121 kind, verdict = classify(value)
122 if what == "node-version-file":
123 kind, verdict = "points at a file", f"open {value.strip()} and read it too"
124 print(f"| {rel}:{line} | {what} | `{value}` | {kind} | {verdict} |")
125 return 0
126
127
128if __name__ == "__main__":
129 sys.exit(main(sys.argv))
I ran it on a fixture repository that I built to look like a small shop: an .nvmrc, a two-stage Dockerfile, a compose file, a package.json and a workflow. Here are the real rows it printed, trimmed to three columns:
| Where | Value | What the script said |
|---|---|---|
ci.yml:6 node-version | 24 | line only, on Wednesday’s list |
ci.yml:11 node-version-file | .nvmrc | points at a file: open it and read it too |
ci.yml:16 node-version | ${{ matrix.node }} | indirect: value set elsewhere, follow it |
.nvmrc:1 | 22 | line only, on Wednesday’s list |
Dockerfile:1 FROM node: | 20-alpine | Node 20, EOL: always affected, not on the list |
Dockerfile:5 FROM node: | lts-trixie-slim | floating: moves when someone re-pulls or rebuilds |
docker-compose.yml:3 | 25 | Node 25, EOL: always affected, not on the list |
package.json:1 engines.node | >=18 | range, not a pin: check what really runs |
Eight rows, and the fixture is tiny. In a real repository the CI file, the Dockerfile and the pin file will not agree with each other. That is the finding. You do not have to fix anything yet. Just see it.
On this site’s repository it printed “NO NODE PINS FOUND” and a warning that this is not the same as safe, which matches what I said above.
Step two: ask the running thing, not the file
A file says what someone wanted. Only the running process says what it got. Files and reality drift apart, so check both before Wednesday.
On any machine where a person or an agent runs commands, three lines tell you the truth:
1node -p "process.version" # the Node on this machine, right now
2which node # which copy, in case there are two
3echo "${NODE_VERSION:-unset}" # some platforms read this variable
In CI, make the job say out loud which Node it got. This is the setup step I would use. It reads the version from the same file your laptop uses, so there is one source of truth, and it prints the answer into the log:
1jobs:
2 test:
3 runs-on: ubuntu-latest
4 steps:
5 - uses: actions/checkout@v7
6 - uses: actions/setup-node@v7
7 with:
8 node-version-file: .nvmrc
9 check-latest: true
10 - name: Print the Node this job really got
11 run: node -p "'node ' + process.version + ' on ' + process.platform"
I parse-checked that YAML with a YAML library. I did not run it on GitHub. Two details come from the actions/setup-node documentation. If you give both node-version and node-version-file, the action uses node-version. And check-latest defaults to false: the action first looks in the runner’s local cache for a match and downloads a newer one only when the cache has nothing. By that description, an older cached patch inside your line can still be what the job uses after a patch release. check-latest: true makes the action confirm the cached copy is the newest first. The documentation notes that this is slower.
For Docker, name the line and rebuild with a fresh pull:
1# Name the line you chose on purpose, not "lts".
2FROM node:24-trixie-slim
3RUN node -p "'built with node ' + process.version"
1docker build --pull --no-cache -t myapp:node-patch .
--pull tells Docker to always try to fetch a newer version of the base image, and --no-cache stops it reusing an old layer. I confirmed both flags in the docker build --help output on my host.
Pin the line, not the patch. A pin to one exact patch is out of date by the time Wednesday ends. The line you chose, plus a rebuild habit, plus a log line that prints the version, is a smaller thing to maintain.

Floating names move without asking you
Some names look like pins and are not. lts, latest and current are moving labels.
The Docker Official Image documentation for node lists which tags point at which build. Today the tags 26, current, latest, lithium, lts and lts-lithium all sit on the same Debian 13 build of the 26 line. By Node’s own schedule, 26 became LTS on 2026-10-28, so an image built from node:lts in September and one built this week can be different major versions. Nobody edited the Dockerfile. The label moved.
Cloudflare Pages does the same thing in a quieter way. Its build image documentation (updated 2026-10-26) says the default Node.js in the v3 build image is 22.23.4, and you override it with a NODE_VERSION environment variable or with an .nvmrc or .node-version file. For minor versions of preinstalled software it says “May be updated to the latest available minor version without notice”, and that applies if you have not overridden the default. It also says the v3 system does not detect the Node version from package.json engines. So the engines row in the table above is not read on that platform.
22.23.4 is also the newest version of the 22 line on nodejs.org today, released on 2026-10-20, so Cloudflare’s default looks current right now. The point is not that it is old. The point is that the version lives in a place you did not write it, and someone else decides when it moves.
The loud part is the old lines
Wednesday’s list names three lines. Your problem is more likely a fourth.
Node 20 reached End-of-Life on 2026-04-30. Node 25 reached End-of-Life on 2026-06-01. The announcement says End-of-Life versions are always affected. When a security release happens, a line the project no longer touches is behind by definition.
In the fixture above, two of eight rows are on an End-of-Life line. In a real repository I expect the old line to hide in the places nobody opens: a worker container, a one-off script image, a CI job added for one release and never touched again. If you find one, the fix is a move to a supported line. It is a planned task with an owner, not a patch. Node 27 is an alpha, so it is not a place to hide from the list.
What I would not do before the details land
- Do not guess the CVEs. The page lists none. Anything you read before Wednesday about what is fixed is a guess.
- Do not let the coding agent “upgrade Node” in one file. That fixes one of five places and makes the other four look fine. Ask for the diff against your list.
Wednesday, once the notes appear
When the advisory text is published, read it with the list open and ask three things:
- Which of my lines are fixed, and to which version?
- What part of Node does each issue touch? Network code and file permissions are different risks from a rarely used built-in.
- Is that part in a request path on one of my servers, or only in a build tool?
Question 3 matters most for a typical Laravel and Vue project, where Node does not serve a single request. It builds the assets. A build step still runs code with your deploy keys in reach, but this tells you the order in which to patch.
What this connects to
This is the same shape as my notes about rules that change on a date. In A Red Fork PR on Nov 9 or 16 May Be a GitHub Rehearsal, Not Your Bug, the answer was a list of workflows with an owner for each. In Your Overnight Agent Job Has a Login With an End Date, it was a list of jobs and the date each login stops working. This post is the same habit for the runtime itself.
It also pairs with A Package Cooldown Also Delays Your Security Fix. A cooldown is a rule about packages. The runtime underneath is not a package in your lockfile, and a late runtime patch costs the same. Long-Running AI Agents: From Demos to Production covers the agent side: name the owner of everything the agent runs on. More notes of this kind are on AI Agent Operations and Developer Tools, and Start Here lists the order to read them.
What I did and did not verify
On 2026-11-10 I read the Node.js announcement page for the November 11 security releases (scraped live), the schedule.json file in nodejs/Release, the release index at nodejs.org/dist/index.json, the Docker Official Image README for node, the Cloudflare Pages build image documentation, and the actions/setup-node README and advanced usage page. I also ran node -p process.version and which node on my own host and looked at this site’s repository.
I ran the scanner on a fixture, on an empty folder, on a missing path (exit code 3) and on this site’s repository. I parse-checked the workflow YAML but did not run it on GitHub, and I did not build the Docker image because the Docker daemon is not running on this host.
I do not know what Wednesday’s issues are, which APIs they touch, or at what hour the releases will ship. The announcement says “on or shortly after”. The release lines, dates and tag lists above can change after today, so re-read the sources before you act on this.
What you should do Monday morning
These fit in one sitting before Wednesday’s release. If you are reading this after it, do them first and then read the advisory.
- Make the list. Five rows: laptop, CI, Docker, build platform, agent host. Add a row for every other place Node runs.
- Run the scanner on every repository you ship. Copy its table into the list. Treat “no pins found” as “version comes from elsewhere”, and go find where.
- Run the three lines on every host that runs commands, including your coding agent’s. Write the real answer next to the file’s answer.
- Find every End-of-Life line. Node 20 and Node 25 get a task with an owner and a date.
- Replace floating names (
lts,latest,current) with a named line, and make CI and the Docker build print the version. - Check your build platform’s default. If you never set
NODE_VERSIONor a pin file, write down that the platform decides. - Name one person per row who patches it. One name, not a team.
- Tell your agent in writing not to change a Node version in one file without showing the diff against the list.
- Open the advisory with the list next to it when it lands on Wednesday.
