Guide · Backend
Node.js's October 2026 Security Release Explained: NUL Bytes, Odd IP Spellings
24 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- The Node.js security release October 2026 is v26.12.1, v24.22.1 (LTS) and v22.23.4 (LTS), out on 21 October 2026. It fixes 10 CVEs, the public IDs for security flaws.
- Four are rated High. One is in dns.resolve(), the Node function that asks a DNS server about a name. It cut names short at a NUL byte, an invisible zero character, so it could look up a name your check never approved.
- Two High bugs let odd ways of writing an IP address, like 127.1, pass the TLS hostname check. That check asks whether a server's certificate really belongs to the address you meant.
- The fix is an upgrade: run node -v and move to one of the three versions. Node.js 20 and 25 are end-of-life, which means they no longer get fixes, so this release skips them.
- Beginners can stop after the upgrade steps. Engineers then get our test results on the builds before and after the fix, why fetch() was not fooled, a snapshot that repeats UUIDs and a CI version gate.
What goes wrong when a security check and the code after it read one string differently?
A check approves a value, then a lower layer reads the same value differently and acts on something else. Nobody sees an error, because each part did what it thinks the string says.
That gap is the thread through the Node.js security release October 2026. Three of its four High bugs come from it. One sits in DNS lookups and two in the TLS certificate check.
Think of a delivery slip checked by a gate guard, then read by a driver. The guard reads the whole slip and waves it through. The driver follows only the first line. Here the guard is your check, the driver is the lower layer and the slip is the string.
Where the picture breaks: a real driver might notice something odd, but code never asks. So the fixes in this release make the guard refuse any slip that could be read two ways.
Picture a common setup. A payments backend sends webhooks, small HTTP calls that tell a merchant's server about each payment. Each merchant saves a delivery host name in a JSON settings form. The setup is made up, but the bug it meets is real.
Before each delivery, the service checks that the host ends with the merchant's verified domain. It then resolves the host with dns.resolve4 and refuses private addresses. Finally it connects to the address it got, a common way to stop DNS answers changing between the check and the call.
The service runs Node.js 24.22.0 in a Docker image built from node:24. On 22 October the team needs two answers. Is that check really safe, and what exactly do they upgrade?
Which words do you need before reading the Node.js advisory?
- CVE: a public ID for one security flaw, such as CVE-2026-105482. Severity (High, Medium or Low) is the Node.js project's own rating on its advisory page.
- LTS, Current and end-of-life: an LTS (long-term support) line gets fixes for about 30 months after LTS starts. The Current line gets new features first. An end-of-life (EOL) line gets nothing, including security fixes.
- NUL byte: the character with code zero, written \u0000 in JavaScript. It is invisible when printed. Code written in C treats it as the end of a string.
- dns.resolve() and dns.lookup(): resolve functions always send a query to a DNS server. lookup asks the operating system, the same way most other programs on the machine do.
- c-ares: the C library that sends the queries behind dns.resolve() in Node.js. Node passes your name string down to it.
- TLS certificate: the file a server shows to prove who it is. Its subject alternative name (SAN) field lists DNS-name entries, like api.example.com, and IP-address entries, like 10.0.0.5.
- Hostname check: after the handshake, Node compares the host you connected to with those entries. tls.checkServerIdentity() does this, and tls.connect() uses it by default.
- Canonical IP: the one standard spelling of an IPv4 address, four decimal numbers like 127.0.0.1. Other spellings, like 127.1, can mean the same address to some parsers.
What does the Node.js security release October 2026 fix?
It fixes 10 CVEs across three release lines: four High, three Medium and three Low. The advisory gives each flaw a one-line title only, so the table keeps to those titles.
Not every flaw is in every line. Three exist only in 26.x and 24.x or only in 26.x, so check your own line in the Affected column.
| CVE | Severity | What goes wrong (advisory title, shortened) | Affected lines | Fixed in |
|---|---|---|---|---|
| CVE-2026-105482 | High | dns.resolve truncates names that contain a NUL character | 26.x, 24.x, 22.x | 26.12.1, 24.22.1, 22.23.4 |
| CVE-2026-105483 | High | TLS hostname check bypass when the IP is written in fullwidth digits | 26.x, 24.x | 26.12.1, 24.22.1 |
| CVE-2026-106069 | High | TLS hostname check bypass when the host loosely resolves to an IP address | 26.x, 24.x, 22.x | 26.12.1, 24.22.1, 22.23.4 |
| CVE-2026-111497 | High | Out-of-bounds write in the Brotli decoder, which the advisory says permits remote code execution | 26.x, 24.x, 22.x | 26.12.1, 24.22.1, 22.23.4 |
| CVE-2026-105481 | Medium | A keep-alive socket processes a response written before the request was sent | 26.x, 24.x, 22.x | 26.12.1, 24.22.1, 22.23.4 |
| CVE-2026-106072 | Medium | Non-canonical IPv4 host names let DNS-name entries authenticate IP endpoints | 26.x, 24.x, 22.x | 26.12.1, 24.22.1, 22.23.4 |
| CVE-2026-106074 | Medium | randomUUID() and randomInt() caches are saved into startup snapshots and single executable apps, so processes repeat values | 26.x, 24.x, 22.x | 26.12.1, 24.22.1, 22.23.4 |
| CVE-2026-106070 | Low | ZipFile returns attacker-controlled prefix bytes after a failed ZIP header check | 26.x only | 26.12.1 |
| CVE-2026-106071 | Low | Permission Model grants too much file access through a wildcard lookup | 26.x only | 26.12.1 |
| CVE-2026-106073 | Low | Permission Model bypass with fs.realpathSync | 26.x, 24.x, 22.x | 26.12.1, 24.22.1, 22.23.4 |
The same releases update bundled parts too. All three get c-ares 1.34.9, an updated undici (the HTTP client behind fetch()) and new root certificates. The advisory links the certificate change to the eMudhra intermediate CA compromise.
The 24.x and 22.x lines also move to OpenSSL 3.5.9, and 22.x gets a new SQLite. On our machine, 26.12.0 already reported OpenSSL 3.5.9, which matches the advisory listing OpenSSL for 24.x and 22.x only.
How did a NUL byte make dns.resolve look up a different name?
Node checked that the name was a string, then passed it to c-ares as a C string, which ends at the first NUL byte. So JavaScript saw one name and the DNS query carried a shorter one.
In the 24.22.0 source, the resolver runs validateString(name) and hands the name to the c-ares binding. A string can legally hold a NUL in JavaScript, so that check passes.
Back to the webhook service. A merchant saves the host example.com\u0000.shop.example, where shop.example is their verified domain. A JSON field carries that NUL without complaint.
// The name a caller passes in, with a NUL byte (\u0000) in the middle
const name = 'example.com\u0000.shop.example';
// An allow-list check written in JavaScript sees the whole string
console.log('passes the check:', name.endsWith('.shop.example'));
// A C library reads a string only up to the first NUL byte
console.log('what C code reads:', name.split('\u0000')[0]);Run it and you get two lines: passes the check: true, then what C code reads: example.com. The allow-list approved one name, while the C side sees another.
We then ran dns.promises.Resolver against a tiny DNS server of our own on 127.0.0.1. On 24.22.0, resolve4() returned an answer, and our server logged a query for example.com. The text after the NUL never left the process.
So the webhook service would resolve example.com, a domain the merchant never verified, and treat its address as approved. Any domain at all could sit before the NUL.
One name, two readers: the dns.resolve NUL-byte bug
Drag to exploreNotice that the allow-list and c-ares read the same string. In 24.22.1 the validator stops it before c-ares sees it.
Diagram connections:
- Merchant's saved host to Allow-list in JavaScript
- Allow-list in JavaScript to dns.resolve4 in 24.22.0 (Approved, old build)
- dns.resolve4 in 24.22.0 to c-ares, a C library
- c-ares, a C library to DNS server (Query for a shorter name)
- Allow-list in JavaScript to dns.resolve4 in 24.22.1 (Approved, fixed build)
- Merchant's saved host: example.com, NUL, .shop.example
- Allow-list in JavaScript: endsWith('.shop.example') is true
- dns.resolve4 in 24.22.0: Checks it is a string, passes it on
- c-ares, a C library: Reads only up to the NUL byte
- DNS server: Gets a query for example.com
- dns.resolve4 in 24.22.1: Throws ERR_INVALID_ARG_VALUE, sends nothing
The fix, commit 6b78ea171d by James M Snell, swaps validateString for validateStringWithoutNullBytes. It covers the callback and promise resolvers, and setLocalAddress().
On 24.22.1 the same call throws a TypeError with code ERR_INVALID_ARG_VALUE. The message reads: The argument 'name' must be a string without null bytes. Our DNS server saw zero queries.
dns.lookup() was not part of this bug. It already refused the NUL name with ERR_INVALID_ARG_VALUE on 22.23.3, 24.22.0 and 26.12.0. In the 24.22.1 source, net sockets use dns.lookup() unless you pass your own lookup function, and http, https and tls connections are built on net sockets.
How did odd IP spellings get past the TLS hostname check?
Node rewrote odd spellings of an IP address into the standard form, then compared that result with the certificate. Depending on the version, a certificate could match an address it was never issued for.
One IPv4 address has many spellings. 127.1, 0x7f.0.0.1 and 2130706433 all mean 127.0.0.1 under the WHATWG URL Standard, the parsing rules browsers follow. A leading zero means octal there, so 0127.0.0.1 becomes 87.0.0.1.
Node's check first passes the host through url.domainToASCII(), and our tests show it applies those IPv4 rules. The trouble is what happened next, and it changed between versions.
What 24.22.0 and 26.12.0 did
These builds first asked net.isIP() whether the host, as written, was an IP. For 127.1 the answer is no, so Node treated it as a domain name.
It then compared the normalised form, 127.0.0.1, with the DNS-name entries. A certificate with a DNS-name entry spelled 127.0.0.1 matched, though it has no IP-address entry at all.
The advisory calls this improper certificate validation, because DNS-name entries should not vouch for an IP address. Pause and predict: the same certificate, two spellings of one address. Which one passes on 24.22.0?
const tls = require('node:tls');
// A fake certificate: only the field this check reads.
// It has a DNS-name entry spelled like an IP, and no IP-address entry.
const cert = { subjectaltname: 'DNS:127.0.0.1' };
for (const host of ['127.0.0.1', '127.1']) {
const err = tls.checkServerIdentity(host, cert);
console.log(process.version, host, err ? 'rejected: ' + err.reason : 'accepted');
}On 24.22.0 and 26.12.0 this prints rejected for 127.0.0.1 and accepted for 127.1. The same certificate fails for the plain spelling and passes for the odd one.
We repeated it end to end on localhost, modelled on the fix's own test. Our own server used a self-signed certificate with only DNS:127.0.0.1. On 24.22.0, https.get() with host 127.1 or 0x7f.0.0.1 connected and got HTTP 200.
What just happened? The check never compared 127.1 with an IP-address entry at all. It took the DNS-name path and matched a name that only looks like an IP.
This path is the 26.x and 24.x regression behind CVE-2026-105483. The advisory names fullwidth digits, the wide forms of 0 to 9 used in Chinese and Japanese text. Our fullwidth 127.0.0.1 matched DNS:127.0.0.1 on 24.22.0 too.
On 24.x and 26.x, two fixes close this one path. The CVE-2026-105483 fix stops a normalised IP reaching the DNS-name check, and the fix for CVE-2026-106069 and CVE-2026-106072 refuses non-canonical IP hosts outright.
A certificate with no DNS-name entries is checked against its Common Name (CN) field instead. On 24.22.0, a fake certificate with only CN 127.0.0.1 was accepted for 127.1 too, and the fixed builds refused it.
What 22.23.3, 24.19.0 and 26.5.1 did
The older code matched the normalised form against IP-address entries. A certificate for the IP 87.0.0.1 passed for the host 0127.0.0.1 on all three builds.
That normalising step was itself a security fix. The change log lists CVE-2026-48618, tls: normalize hostname for server identity checks, rated High, in 22.23.0, 24.17.0 and 26.3.1 on 18 June 2026.
It added a second reader of the host string, and the October fixes deal with what that second reader got wrong.
The fix commit explains the danger: the operating system's resolver may read the original string as a different address. On our Linux test machine both read 87.0.0.1, so we could not show a mismatch there.
The advisory also lists 22.x for CVE-2026-106072, the DNS-name case. None of our odd spellings reproduced that case on 22.23.3, so we only report the advisory's word for it.
How the fixed hostname check treats a host like 127.1
Drag to exploreNotice the new question in the middle. A host that becomes an IP only after normalising is refused before any entry is compared.
Diagram connections:
- Host string 127.1 to An IP as written?
- An IP as written? to Match IP-address entries (Yes)
- An IP as written? to Normalise the host (No)
- Normalise the host to An IP only now?
- An IP only now? to Refuse the certificate (Yes, in fixed builds)
- An IP only now? to Match DNS-name entries (No)
- Normalise the host to Old path in 24.22.0 (24.22.0 skipped the question)
- Host string 127.1: From config, user input or url.parse()
- An IP as written?: net.isIP('127.1') says no
- Match IP-address entries: Plain spellings such as 127.0.0.1
- Normalise the host: domainToASCII gives 127.0.0.1
- An IP only now?: Question added in the October fix
- Refuse the certificate: Host is a non-canonical IP address
- Match DNS-name entries: Real names such as api.example.com
- Old path in 24.22.0: Matched DNS:127.0.0.1 for host 127.1
The fixed builds refuse any host that becomes an IP only after normalising. Commit ad788ccd07 by Matteo Collina adds that check, with error reason Host is a non-canonical IP address.
Its tests cover leading-zero, hex, single-number and shortened forms such as 127.1, plus fullwidth digits and the ideographic full stop. On all three fixed builds, every odd spelling we tried was refused.
That held against DNS-name and IP-address entries alike. A trailing dot, as in 127.0.0.1., still counts as the plain address and matches its IP-address entry.
Who can actually abuse it
An attacker still needs a certificate your client trusts. The bug widens which trusted certificates count as a match, for hosts written in an odd way.
The severity is also disputed in the record itself. The advisory rates CVE-2026-105483 High. Its fix commit, cd4b05469c, calls the regression a low-severity bug that is not easy to exploit.
How do you check your Node.js version and upgrade?
Print the version on every machine and image that runs your code, then move each one to the fixed build for its line.
Print the version
Run node -v. Fixed output is v26.12.1, v24.22.1 or v22.23.4, or anything later on the same line. Inside a running app, log process.version at startup.
Find your line
The first number is the line. Lines 22, 24 and 26 have fixes. Every line below 22, plus 23 and 25, is end-of-life, so plan a move to 24 or 26.
Upgrade with nvm on laptops
If you use nvm, run nvm install 24.22.1, then nvm alias default 24.22.1 so new shells use it. Run node -v again to confirm.
Upgrade on plain Windows
nvm's README lists Unix, macOS and Windows WSL as its platforms. Outside WSL, run the Windows installer linked from the release notes for your version, then check node -v.
Rebuild Docker images
A tag like node:24 moves when the image is rebuilt upstream, but your built image does not. Rebuild with docker build --pull so Docker fetches the newest base image.
Know what the tags hold
As of 23 October 2026, Docker Hub lists node:24, node:24-bookworm and node:24-alpine with the same digests as their 24.22.1 tags. For 22.23.4 and 26.12.1, the page showed only -alpine3.24 variants.
Check inside the image
Run docker run --rm YOUR_IMAGE node -v. If it still prints v24.22.0, look for a FROM line pinned to an old tag or digest, a registry mirror that lags, or a reused CI image.
Pin if you want control
FROM node:24.22.1-bookworm fixes the exact version. You then have to bump it yourself, so pair it with a dependency bot such as Dependabot or Renovate.
Check distribution packages
Packages from apt or dnf follow their distribution's own schedule. Read the package changelog for these CVE IDs rather than trusting the version number alone.
Restart the processes
Installing a new Node binary does not change a process that is already running. Restart each service, then check the logged process.version.
# Which Node.js is this?
node -v # prints v24.22.1 on a fixed 24.x machine
node -p "process.versions.node" # prints 24.22.1, without the v
# nvm users (laptops and dev boxes)
nvm install 24.22.1
nvm alias default 24.22.1
# Docker: fetch the newest base image, then look inside
docker build --pull -t my-api .
docker run --rm my-api node -vFor the webhook service, the image was built from node:24 on 15 October (made up), so it still holds 24.22.0. The team rebuilds with --pull, checks that the image prints v24.22.1, and adds a host-field check.
What changed between 24.22.0 and 24.22.1 in our tests?
Each row compares the build before the fix with the fixed build on each line. nodejs.org downloads were blocked from our test machine, so we used the Linux x64 builds published on npm as node-linux-x64.
Each one printed the expected version with node -v. Every test used fake certificate objects, our own self-signed certificate, or small servers on localhost.
| Test | 22.23.3 | 22.23.4 | 24.22.0 and 26.12.0 | 24.22.1 and 26.12.1 |
|---|---|---|---|---|
| resolve4 of a name with a NUL byte | Query sent for example.com | ERR_INVALID_ARG_VALUE | Query sent for example.com | ERR_INVALID_ARG_VALUE |
| Host 127.1, cert with only DNS:127.0.0.1 | Rejected | Rejected | Accepted | Rejected |
| Host 0127.0.0.1, cert with only IP 87.0.0.1 | Accepted | Rejected | Rejected | Rejected |
| Fullwidth 127.0.0.1, cert with only DNS:127.0.0.1 | Rejected | Rejected | Accepted | Rejected |
| Host ::1, cert with IP ::1 | Rejected | Accepted | Accepted | Accepted |
| Two starts of one snapshot print the same UUID | Yes | No | Yes | No |
The ::1 row is not a security fix. 22.23.4 also brings an earlier change that lets IPv6 hosts match IP-address entries. That same change is what opened the regression on 24.x and 26.x.
We found where that regression started by testing older builds. Fullwidth digits matched a DNS-name entry from 24.20.0 and 26.6.0 onwards, but not on 24.19.0 or 26.5.1.
We then compared the built-in tls module source across each boundary. From 24.19.0 to 24.20.0, and from 26.5.1 to 26.6.0, the only change is that hunk: net.isIP() now tests the host as written.
The change log credits that change to pull request 64145, which shipped in 24.20.0 on 26 August 2026 and 26.6.0 on 3 August 2026. The advisory gives no version ranges, so this is our own finding.
Why was fetch() not fooled when https.get() was?
fetch() takes a URL, and the URL parser rewrites the host to its standard form before any connection. So 0x7f.0.0.1 becomes 127.0.0.1, which takes the normal IP path.
In our localhost test, fetch() to https://0x7f.0.0.1 was refused with ERR_TLS_CERT_ALTNAME_INVALID on all six builds. https.get() with the raw host connected on 24.22.0 and 26.12.0.
The legacy url.parse() keeps ASCII odd spellings such as 127.1 and 0x7f.0.0.1, so its output carries the risk into https.get(). It did turn fullwidth digits into 127.0.0.1 in our runs.
On 24.x and 26.x, Node prints a runtime deprecation warning, DEP0169, for url.parse(), saying CVEs are not issued for its flaws. Our 22.x runs printed no such warning.
So the exposed code passes a host string it did not build into https.request(), tls.connect() or checkServerIdentity(). Hosts from config files, service registries and user input are the usual sources.
Are Node.js 20 and 25 still affected after October 2026?
They get no fix, and the NUL-byte bug is still there. Node.js 20 reached end-of-life on 30 April 2026 and 25 on 1 June 2026. Their last builds are 20.20.2 and 25.9.0.
On both, our resolve4 test sent a query for example.com, the same truncation. The advisory says end-of-life versions are always affected when a security release occurs.
It goes further there. On 20.20.2 and 25.9.0, dns.lookup() also cut the name at the NUL: localhost followed by a NUL and .shop.example resolved to 127.0.0.1. So did net.connect() with that host.
http.get() threw ERR_INVALID_CHAR there only with its default Host header, which would carry the NUL. With its own Host header set, it got HTTP 200 from the truncated name, and tls.connect() reached it too.
This lookup behaviour is what we saw on those builds, not part of the advisory's CVE text. On 24.22.0 and 24.22.1, both calls threw ERR_INVALID_ARG_VALUE instead.
The TLS story differs there. Their checkServerIdentity() does not normalise the host at all, since both predate the June normalising fix, and both rejected every odd spelling we tried. We did not test the Brotli, keep-alive or snapshot flaws on them.
That still leaves them unpatched for at least one High bug, with no future fixes. Moving to 24.x is the shorter jump from 20.x, and 26.x is scheduled to become LTS on 28 October 2026.
What do the Medium and Low fixes mean for snapshots, Brotli and permissions?
Startup snapshots that repeat random values
A startup snapshot saves a warmed-up heap, the app's memory, to a file so later starts skip set-up work. The advisory says single executable applications (SEAs), apps bundled into one binary, are affected too.
Node keeps a cache of random bytes for randomUUID() and randomInt(). If code calls either while the snapshot is built, the filled cache is saved too. Every start then hands out the same values from that cache.
We built a snapshot that called randomUUID() once during the build. On 22.23.3, 24.22.0 and 26.12.0, two separate starts printed the same UUID and the same randomInt() result. On the fixed builds both differed.
Without that build-time call, 24.22.0 printed different UUIDs on each start. So in our test the repeat needed a random call during the build. We did not build an SEA.
Brotli and the version number you cannot trust
Brotli is a compression format that web servers use for responses. Any code that decompresses Brotli data from outside runs the decoder that had the out-of-bounds write, a write past the end of a memory buffer.
fetch() decompresses Brotli responses for you. The bundled undici code in 24.22.1 calls zlib.createBrotliDecompress for a br encoding, and our local fetch() decoded one.
The fix is a cherry-pick of upstream Brotli commit 0ed4268a44, so process.versions.brotli does not change. It still printed 1.2.0 on 24.22.0 and 24.22.1, and 1.1.0 on both 22.x builds.
One more signal we found: zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH is -17 on the three fixed builds and undefined on the three before. It comes from a companion commit in the same fix.
The keep-alive and permission fixes
The keep-alive fix is in the http agent, the part of Node that reuses connections between requests. The advisory title says a reused socket could process a response written before the request was sent.
The Permission Model is an opt-in sandbox started with the --permission flag. Its 26.x-only wildcard bug and the realpathSync bypass matter only if you rely on it to limit file access.
ZipFile is a zlib class for reading ZIP archives that only 26.x has. In our runs, 26.12.1 had zlib.ZipFile and 24.22.1 did not, so the ZIP fix concerns 26.x users only.
How do you gate deploys on the patched Node.js version?
Fail the build when the Node.js running it is older than the fixed release for its line. Run this as the first CI step and inside each image.
// check-node.js: is this Node.js on an October 2026 security release or later?
const fixed = { 22: [23, 4], 24: [22, 1], 26: [12, 1] }; // first fixed minor, patch
const [major, minor, patch] = process.versions.node.split('.').map(Number);
const need = fixed[major];
if (major > 26) {
console.log(`${process.version}: newer line, check its own release notes.`);
} else if (!need) {
console.log(`${process.version}: end-of-life line, move to 22, 24 or 26.`);
process.exitCode = 1;
} else if (minor > need[0] || (minor === need[0] && patch >= need[1])) {
console.log(`${process.version}: has the October 2026 fixes.`);
} else {
console.log(`${process.version}: upgrade to v${major}.${need[0]}.${need[1]} or later.`);
process.exitCode = 1;
}We ran it on nine builds. 22.23.3, 24.21.0, 24.22.0 and 26.12.0 printed an upgrade line and exited with code 1. The three fixed builds exited 0, while 20.20.2 and 25.9.0 exited 1 as end-of-life.
Then look for the code paths these bugs touch, so you know which services to patch first. This grep finds DNS resolve and reverse calls, setLocalAddress(), custom Resolvers, direct identity checks and the legacy URL parser.
grep -rnE "\.resolve(4|6|Any|Caa|Cname|Mx|Naptr|Ns|Ptr|Soa|Srv|Tlsa|Txt)?\(|\.reverse\([^)]|setLocalAddress\(|Resolver\(|checkServerIdentity|url\.parse\(" src/ \
| grep -vE "(Promise|path|require|url)\.resolve\("On a test folder it listed all seven planted calls, including a commented-out checkServerIdentity line. It skipped Promise.resolve(), path.resolve(), require.resolve(), url.resolve() and a plain array reverse().
It still over-reports, and it misses destructured calls such as const { resolve4 } = require('dns'). A hit means read the code, not that you are exposed.
How do you explain the Node.js October 2026 security release in a system design interview?
Treat it as a story about two parts of a system reading one input differently. These steps give you the frame.
Name the pattern
A check and the code after it disagreed about one string. This is often called a parser differential.
Give the two cases
JavaScript saw the whole name, while c-ares stopped at a NUL. The TLS check turned 127.1 into 127.0.0.1, then matched it as if it were a name.
Explain the fix style
Both fixes refuse ambiguous input at the boundary. They do not try to guess which reading is right.
Add your own defence
Canonicalise once with new URL(), validate host fields, and connect using that one canonical value everywhere.
Describe the rollout
Inventory versions with node -v, rebuild images with --pull, gate CI on the fixed version and restart services.
Questions people ask
- Which Node.js version should I install after the October 2026 security release?
- Install the fixed build on your current line: 26.12.1, 24.22.1 or 22.23.4. If you are on 20.x or 25.x, those lines are end-of-life, so move to 24.x or 26.x. As of 23 October 2026, 24.x is in maintenance LTS until April 2028, and 26.x is scheduled to become LTS on 28 October.
- Am I affected if my app never calls dns.resolve?
- On 22.x, 24.x and 26.x, not by the NUL-byte bug, which is in the dns.resolve family, reverse() and setLocalAddress(). There, dns.lookup(), the default for net, http and https, already refused NUL names in our tests. On end-of-life 20.20.2 and 25.9.0, dns.lookup() also cut the name, so move off them. Upgrade anyway for the other flaws.
- Does fetch() need this update?
- Yes, for other reasons. fetch() rewrites odd IP spellings before connecting, and it was not fooled in our TLS test. But it decodes Brotli responses with Node's own decoder, which had the High out-of-bounds write. It also ships in the bundled undici, which these releases update.
- How do I check the Node.js version inside a Docker image?
- Run docker run --rm YOUR_IMAGE node -v. An image built before 21 October 2026 still holds the old build, even if its tag now points at a newer one. As of 23 October 2026, Docker Hub's node:24 shares its digest with node:24.22.1. Rebuild with docker build --pull, then check again.
- Why is CVE-2026-105483 not in Node.js 22?
- It comes from a regression that reached only the 24.x and 26.x lines. A change for IPv6 hosts shipped there in August 2026 and sent hosts like fullwidth digits down the DNS-name path. 22.x received that change only in 22.23.4, together with the stricter fix, so the regression never shipped there.
- Is the Brotli bug fixed if process.versions.brotli is unchanged?
- Yes, if process.version is one of the fixed builds. The fix is a cherry-picked upstream patch, so the reported Brotli version stays 1.2.0 on 24.x and 26.x, and 1.1.0 on 22.x. Judge by the Node.js version, not the library version.
Sources and further reading
- Node.js security releases, 21 October 2026 (advisory: CVEs, severities, affected lines)
- Node.js v24.22.1 release notes (21 October 2026)
- Node.js v26.12.1 release notes (21 October 2026)
- Node.js v22.23.4 release notes (21 October 2026)
- Commit ad788ccd07: tls: reject non-canonical IP addresses (CVE-2026-106069, CVE-2026-106072)
- Commit cd4b05469c: tls: fix ip normalization regression (CVE-2026-105483)
- Commit 6b78ea171d: dns: validate resolve inputs for null bytes (CVE-2026-105482)
- Node.js release schedule (LTS and end-of-life dates)
- Node.js previous releases (latest 20.x and 25.x builds)
- Node.js DNS documentation (dns.lookup vs dns.resolve)
- Node.js zlib documentation (Brotli constants, zlib.ZipFile)
- nvm README (nvm install, nvm alias default, supported platforms)
- Docker Hub official node image tags (node:24 and 24.22.1 digests, 23 October 2026)