Commit 9731991b3c8 for nodejs

commit 9731991b3c8c8ea1c917cdec7c1049cbed63d943
Author: James M Snell <jasnell@gmail.com>
Date:   Sat Sep 19 20:56:11 2026 +0000

    tls: fix ip normalization regression

    The regression is a low-severity bug that's not easy to
    exploit. Basically it just means the hostname check can
    be bypassed.

    Signed-off-by: James M Snell <jasnell@gmail.com>
    PR-URL: https://github.com/nodejs-private/node-private/pull/985
    Fixes: https://hackerone.com/reports/4038287
    Reviewed-By: Robert Nagy <ronagy@icloud.com>
    Reviewed-By: Rich Trott <rtrott@gmail.com>
    Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
    CVE-ID: CVE-2026-105483

diff --git a/lib/tls.js b/lib/tls.js
index a1d978c50f2..42562108eaa 100644
--- a/lib/tls.js
+++ b/lib/tls.js
@@ -456,14 +456,26 @@ exports.checkServerIdentity = function checkServerIdentity(hostname, cert) {
   let valid = false;
   let reason = 'Unknown reason';

-  // An IP literal must not be IDNA-normalized: domainToASCII() returns '' for
-  // an IPv6 literal (it is not a domain), so matching against the normalized
-  // host would skip IP-SAN matching for IPv6 entirely. Match IP hosts against
-  // the original hostname (net.isIP() rejects non-ASCII, so there is no IDNA
-  // confusion to guard against here); the normalized form is kept for the
-  // DNS-name path below.
+  // Decide whether the host is an IP address by checking both forms of it,
+  // because neither form is enough on its own:
+  //
+  // - The original hostname is needed for IPv6. domainToASCII() returns '' for
+  //   an IPv6 literal such as '::1' (it is not a domain).
+  // - The normalized hostname is needed for hosts that net.isIP() rejects but
+  //   that normalize to an IPv4 address: '\uFF11\uFF12\uFF17.0.0.1' (fullwidth
+  //   digits), '127\u30020.0.1' (ideographic full stop) and '127.1' all become
+  //   '127.0.0.1'. The resolver treats them as that address too, so they must
+  //   be matched against IP SANs. Letting them fall through to the DNS-name
+  //   path would match an IP address against DNS names and the CN instead.
+  let ip = '';
   if (net.isIP(hostname)) {
-    valid = ips.includes(canonicalizeIP(hostname));
+    ip = hostname;
+  } else if (net.isIP(hostnameASCIIWithoutFQDN)) {
+    ip = hostnameASCIIWithoutFQDN;
+  }
+
+  if (ip) {
+    valid = ips.includes(canonicalizeIP(ip));
     if (!valid) {
       reason =
         `IP: ${hostname} is not in the cert's list: ` + ips.join(', ');
diff --git a/test/parallel/test-tls-check-server-identity.js b/test/parallel/test-tls-check-server-identity.js
index e21f39ccc76..61a2513f81d 100644
--- a/test/parallel/test-tls-check-server-identity.js
+++ b/test/parallel/test-tls-check-server-identity.js
@@ -394,6 +394,47 @@ const tests = [
     },
     error: 'IP: 127.0.0.1 is not in the cert\'s list: '
   },
+  // A host that net.isIP() rejects but that normalizes to an IPv4 address is
+  // an IP address, not a DNS name: it is matched against "IP Address:" SANs,
+  // never against DNS names or the CN.
+  {
+    host: '\uFF11\uFF12\uFF17.0.0.1', // "127.0.0.1" with fullwidth digits.
+    cert: { subjectaltname: 'DNS:127.0.0.1', subject: {} },
+    error: 'IP: \uFF11\uFF12\uFF17.0.0.1 is not in the cert\'s list: '
+  },
+  {
+    host: '\uFF11\uFF12\uFF17.0.0.1',
+    cert: { subjectaltname: 'DNS:*.0.0.1', subject: {} },
+    error: 'IP: \uFF11\uFF12\uFF17.0.0.1 is not in the cert\'s list: '
+  },
+  {
+    host: '\uFF11\uFF12\uFF17.0.0.1',
+    cert: { subject: { CN: '127.0.0.1' } },
+    error: 'IP: \uFF11\uFF12\uFF17.0.0.1 is not in the cert\'s list: '
+  },
+  {
+    host: '\uFF11\uFF12\uFF17.0.0.1',
+    cert: { subjectaltname: 'IP Address:127.0.0.1', subject: {} }
+  },
+  {
+    host: '\uFF11\uFF12\uFF17.0.0.1',
+    cert: { subjectaltname: 'IP Address:127.0.0.2', subject: {} },
+    error: 'IP: \uFF11\uFF12\uFF17.0.0.1 is not in the cert\'s list: 127.0.0.2'
+  },
+  {
+    host: '127\u30020.0.1', // "127.0.0.1" with an ideographic full stop.
+    cert: { subjectaltname: 'DNS:127.0.0.1', subject: {} },
+    error: 'IP: 127\u30020.0.1 is not in the cert\'s list: '
+  },
+  {
+    host: '127.1',
+    cert: { subjectaltname: 'DNS:127.0.0.1', subject: {} },
+    error: 'IP: 127.1 is not in the cert\'s list: '
+  },
+  {
+    host: '127.1',
+    cert: { subjectaltname: 'IP Address:127.0.0.1', subject: {} }
+  },
   {
     host: 'localhost', cert: {
       subjectaltname: 'DNS:a.com',