Version
v24.17.0, v24.18.0, v22.23.1, v26.4.0 (and the other CVE-2026-48618 security releases). Last good: v24.16.0.
Platform
All (logic-only; reproduced on Linux x86-64).
Subsystem
tls
What steps will reproduce the bug?
tls.checkServerIdentity() no longer matches an IPv6 host against a matching IP Address SAN. It returns ERR_TLS_CERT_ALTNAME_INVALID (Cert does not contain a DNS name) where it used to return undefined.
const tls = require('node:tls');
const result = tls.checkServerIdentity('::1', {
subject: {},
subjectaltname: 'IP Address:::1',
});
console.log(result === undefined ? 'OK (matched)' : `BROKEN: ${result.reason}`);
Across versions:
v24.16.0 -> OK (matched) // last good
v24.17.0 -> BROKEN: Cert does not contain a DNS name // first broken (24.x)
v24.18.0 -> BROKEN
v22.23.1 -> BROKEN
v26.4.0 -> BROKEN
How often does it reproduce? Is there a required configuration?
100% on the affected versions. No configuration required.
What is the expected behavior? Why is that the expected behavior?
undefined (a successful match): the host ::1 is an IPv6 literal and the certificate carries that exact IP in an IP Address SAN, so server-identity verification should pass — as it did through v24.16.0. IPv4 (IP Address:1.2.3.4 for host 1.2.3.4) still works, so the IP-SAN matching path is expected to work for IPv6 too.
What do you see instead?
ERR_TLS_CERT_ALTNAME_INVALID with reason Cert does not contain a DNS name — i.e. the IP-SAN matching branch is skipped entirely and it falls through to the no-identifier fallback.
Additional information
Root cause. This regressed in CVE-2026-48618 — commit 1efb4ff51a0 “tls: normalize hostname for server identity checks”. That change moved the IP gate from the original hostname to the IDNA-normalized one:
- hostname = unfqdn(hostname);
- if (net.isIP(hostname)) {
- valid = ips.includes(canonicalizeIP(hostname));
+ if (net.isIP(hostnameASCIIWithoutFQDN)) {
+ valid = ips.includes(canonicalizeIP(hostnameASCIIWithoutFQDN));
where hostnameASCII = domainToASCII(hostname) (lib/tls.js). But domainToASCII('::1') === '' — an IPv6 literal is not a valid domain — so net.isIP('') is 0, the IP branch is skipped, and (with no DNS SAN and no CN) it returns Cert does not contain a DNS name. IPv4 is unaffected because dotted-decimal survives domainToASCII (domainToASCII('1.2.3.4') === '1.2.3.4').
const { domainToASCII } = require('node:url');
const net = require('node:net');
domainToASCII('::1'); // '' -> net.isIP('') === 0 (IPv6 IP-SAN matching skipped)
domainToASCII('1.2.3.4'); // '1.2.3.4' -> net.isIP(...) === 4 (IPv4 still works)
Impact. A TLS client connecting to an IPv6 literal whose certificate carries that address in an IP Address SAN now fails server-identity verification. This is fail-closed (no security hole), but it breaks a legitimate IPv6 TLS use case, and tls.checkServerIdentity() is public, documented API.
Suggested fix. IDNA normalization should not apply to IP literals. Gate the IP branch on the original hostname so IPv6 (and IPv4) literals bypass domainToASCII, while keeping the normalization for the DNS-name branch (which is what the CVE fix is actually about):
if (net.isIP(hostname)) {
valid = ips.includes(canonicalizeIP(hostname));
// ...
} else if (dnsNames.length > 0 || subject?.CN) {
const hostParts = splitHost(hostnameASCIIWithoutFQDN); // DNS path keeps the normalization
// ...
}
canonicalizeIP() already canonicalizes IP literals, and an IP literal cannot be a Unicode/IDNA confusable, so this preserves the CVE-2026-48618 hardening for DNS names while restoring IPv6 IP-SAN matching.
Refs: CVE-2026-48618, 1efb4ff51a0.
Version
v24.17.0, v24.18.0, v22.23.1, v26.4.0 (and the other CVE-2026-48618 security releases). Last good: v24.16.0.
Platform
All (logic-only; reproduced on Linux x86-64).
Subsystem
tls
What steps will reproduce the bug?
tls.checkServerIdentity()no longer matches an IPv6 host against a matchingIP AddressSAN. It returnsERR_TLS_CERT_ALTNAME_INVALID(Cert does not contain a DNS name) where it used to returnundefined.Across versions:
How often does it reproduce? Is there a required configuration?
100% on the affected versions. No configuration required.
What is the expected behavior? Why is that the expected behavior?
undefined(a successful match): the host::1is an IPv6 literal and the certificate carries that exact IP in anIP AddressSAN, so server-identity verification should pass — as it did through v24.16.0. IPv4 (IP Address:1.2.3.4for host1.2.3.4) still works, so the IP-SAN matching path is expected to work for IPv6 too.What do you see instead?
ERR_TLS_CERT_ALTNAME_INVALIDwith reasonCert does not contain a DNS name— i.e. the IP-SAN matching branch is skipped entirely and it falls through to the no-identifier fallback.Additional information
Root cause. This regressed in CVE-2026-48618 — commit
1efb4ff51a0“tls: normalize hostname for server identity checks”. That change moved the IP gate from the original hostname to the IDNA-normalized one:where
hostnameASCII = domainToASCII(hostname)(lib/tls.js). ButdomainToASCII('::1') === ''— an IPv6 literal is not a valid domain — sonet.isIP('')is0, the IP branch is skipped, and (with no DNS SAN and no CN) it returnsCert does not contain a DNS name. IPv4 is unaffected because dotted-decimal survivesdomainToASCII(domainToASCII('1.2.3.4') === '1.2.3.4').Impact. A TLS client connecting to an IPv6 literal whose certificate carries that address in an
IP AddressSAN now fails server-identity verification. This is fail-closed (no security hole), but it breaks a legitimate IPv6 TLS use case, andtls.checkServerIdentity()is public, documented API.Suggested fix. IDNA normalization should not apply to IP literals. Gate the IP branch on the original hostname so IPv6 (and IPv4) literals bypass
domainToASCII, while keeping the normalization for the DNS-name branch (which is what the CVE fix is actually about):canonicalizeIP()already canonicalizes IP literals, and an IP literal cannot be a Unicode/IDNA confusable, so this preserves the CVE-2026-48618 hardening for DNS names while restoring IPv6 IP-SAN matching.Refs: CVE-2026-48618,
1efb4ff51a0.