Skip to content

tls: checkServerIdentity() no longer matches IPv6 IP-Address SANs (regressed in v24.17.0) #64144

Description

@JumpLink

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions