Title: tls: checkServerIdentity() no longer matches IPv6 IP-Address SANs (regressed in v24.17.0) · Issue #64144 · nodejs/node · GitHub
Open Graph Title: tls: checkServerIdentity() no longer matches IPv6 IP-Address SANs (regressed in v24.17.0) · Issue #64144 · nodejs/node
X Title: tls: checkServerIdentity() no longer matches IPv6 IP-Address SANs (regressed in v24.17.0) · Issue #64144 · nodejs/node
Description: 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.checkServerI...
Open Graph Description: 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 ...
X Description: 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 ...
Opengraph URL: https://github.com/nodejs/node/issues/64144
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"tls: checkServerIdentity() no longer matches IPv6 IP-Address SANs (regressed in v24.17.0)","articleBody":"### Version\n\nv24.17.0, v24.18.0, v22.23.1, v26.4.0 (and the other CVE-2026-48618 security releases). **Last good: v24.16.0.**\n\n### Platform\n\nAll (logic-only; reproduced on Linux x86-64).\n\n### Subsystem\n\ntls\n\n### What steps will reproduce the bug?\n\n`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`.\n\n```js\nconst tls = require('node:tls');\n\nconst result = tls.checkServerIdentity('::1', {\n subject: {},\n subjectaltname: 'IP Address:::1',\n});\n\nconsole.log(result === undefined ? 'OK (matched)' : `BROKEN: ${result.reason}`);\n```\n\nAcross versions:\n\n```\nv24.16.0 -\u003e OK (matched) // last good\nv24.17.0 -\u003e BROKEN: Cert does not contain a DNS name // first broken (24.x)\nv24.18.0 -\u003e BROKEN\nv22.23.1 -\u003e BROKEN\nv26.4.0 -\u003e BROKEN\n```\n\n### How often does it reproduce? Is there a required configuration?\n\n100% on the affected versions. No configuration required.\n\n### What is the expected behavior? Why is that the expected behavior?\n\n`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.\n\n### What do you see instead?\n\n`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.\n\n### Additional information\n\n**Root cause.** This regressed in **CVE-2026-48618** — commit [`1efb4ff51a0`](https://github.com/nodejs/node/commit/1efb4ff51a0624236332ea98b23bd1106f68d8af) *“tls: normalize hostname for server identity checks”*. That change moved the IP gate from the original hostname to the IDNA-normalized one:\n\n```diff\n- hostname = unfqdn(hostname);\n- if (net.isIP(hostname)) {\n- valid = ips.includes(canonicalizeIP(hostname));\n+ if (net.isIP(hostnameASCIIWithoutFQDN)) {\n+ valid = ips.includes(canonicalizeIP(hostnameASCIIWithoutFQDN));\n```\n\nwhere `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'`).\n\n```js\nconst { domainToASCII } = require('node:url');\nconst net = require('node:net');\ndomainToASCII('::1'); // '' -\u003e net.isIP('') === 0 (IPv6 IP-SAN matching skipped)\ndomainToASCII('1.2.3.4'); // '1.2.3.4' -\u003e net.isIP(...) === 4 (IPv4 still works)\n```\n\n**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.\n\n**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):\n\n```js\nif (net.isIP(hostname)) {\n valid = ips.includes(canonicalizeIP(hostname));\n // ...\n} else if (dnsNames.length \u003e 0 || subject?.CN) {\n const hostParts = splitHost(hostnameASCIIWithoutFQDN); // DNS path keeps the normalization\n // ...\n}\n```\n\n`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.\n\nRefs: CVE-2026-48618, [`1efb4ff51a0`](https://github.com/nodejs/node/commit/1efb4ff51a0624236332ea98b23bd1106f68d8af).\n","author":{"url":"https://github.com/JumpLink","@type":"Person","name":"JumpLink"},"datePublished":"2026-06-26T09:58:32.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":0},"url":"https://github.com/64144/node/issues/64144"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:ddf2603a-d04c-5a4e-0913-852f48653f12 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | CE52:119F46:667FDE:9166CF:6A631259 |
| html-safe-nonce | 4f6ac7a7b97afb1cc7aa8a0f23f6235467503355a6425cc08531af21bc52e511 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJDRTUyOjExOUY0Njo2NjdGREU6OTE2NkNGOjZBNjMxMjU5IiwidmlzaXRvcl9pZCI6IjMxMDk2NzgyMDIzNzU4MzYyNDkiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | d7ee31062d29a55b6fbe50035d3e8bae6af53fc4af4d999e1f5450c0fabe257c |
| hovercard-subject-tag | issue:4751078716 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/nodejs/node/64144/issue_layout |
| twitter:image | https://opengraph.githubassets.com/f07b3469f652cc3bc7eaecb4bac5a1fae0ef0ceb4b14906d3a41870b887426a7/nodejs/node/issues/64144 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/f07b3469f652cc3bc7eaecb4bac5a1fae0ef0ceb4b14906d3a41870b887426a7/nodejs/node/issues/64144 |
| og:image:alt | 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 ... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | JumpLink |
| hostname | github.com |
| expected-hostname | github.com |
| None | 1a6c056e02f174fffc096c521ec0ff6fb83e40a2ec8cb8875466ec1524872dd6 |
| turbo-cache-control | no-preview |
| go-import | github.com/nodejs/node git https://github.com/nodejs/node.git |
| octolytics-dimension-user_id | 9950313 |
| octolytics-dimension-user_login | nodejs |
| octolytics-dimension-repository_id | 27193779 |
| octolytics-dimension-repository_nwo | nodejs/node |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 27193779 |
| octolytics-dimension-repository_network_root_nwo | nodejs/node |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 6a93e25585f487ddff9e3996c06d5b869d6e1828 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width