{"id":"GHSA-2vr4-cq9g-pvrc","summary":"ip-address: no classifier recognizes the NAT64 local-use range 64:ff9b:1::/48, allowing SSRF and trust-boundary bypass","details":"### Summary\n\nNo classifier on `Address6` recognizes the NAT64 local-use range `64:ff9b:1::/48` (RFC 8215). `isPrivate()`, `isLoopback()`, `isLinkLocal()` and their siblings all return `false` for every address in it, so an internal IPv4 destination written through a local-use NAT64 prefix (`64:ff9b:1:7f00:0:100::` for `127.0.0.1`, `64:ff9b:1:a9fe:a9:fe00::` for `169.254.169.254`) reads as an ordinary global address. `getType()` names the range `'NAT64 (local-use)'`, so the library knows what the address is and classifies it as nothing.\n\nAn application that builds a network trust-boundary decision on these checks (for example a filter intended to block Server-Side Request Forgery, or SSRF) will classify an internal target as external and allow the request. SSRF is an attack in which a user-supplied address coaxes the server into making a request to an internal destination the user could not otherwise reach, such as a loopback service or a cloud metadata endpoint.\n\n### Details\n\nThe fix for GHSA-22jq-vg5j-6vgg (10.2.1) classifies IPv4-mapped (`::ffff:0:0/96`) and NAT64 well-known (`64:ff9b::/96`) addresses by their embedded IPv4 address: `embeddedIPv4()` in `src/ipv6.ts` decodes the trailing 32 bits and every special-use classifier delegates to the resulting `Address4`. Those are the only two ranges `embeddedIPv4()` handles.\n\nThe local-use range differs in kind from the well-known prefix, which is why extending the decoder does not fix it. RFC 8215 reserves `64:ff9b:1::/48` so an operator can carve their own NAT64 prefix out of it, of any length RFC 6052 allows (`/48`, `/56`, `/64`, or `/96`). Where the IPv4 address sits depends on that choice: `127.0.0.1` is `64:ff9b:1:7f00:0:100::` under a `/48` prefix and `64:ff9b:1::7f00:1` under a `/96` prefix, and each of those decodes to a different address under the other prefix length. There is no single embedded IPv4 address for the library to classify by.\n\nWhat is well-defined is the range as a whole. The IANA IPv6 Special-Purpose Address Registry lists `64:ff9b:1::/48` as not globally reachable, and Python's `ipaddress` module reports `is_private` as `True` and `is_global` as `False` for every address in it. `isPrivate()` covered ULA (`fc00::/7`) plus the decoded mapped and well-known cases, and nothing in the local-use range.\n\n### Affected versions\n\n`\u003e= 10.2.0, \u003c= 10.5.0`. The `is*` classification API was extended to `Address6` in 10.2.0; releases before it do not expose the method and are not affected through this vector.\n\n### Impact\n\nEvery address in `64:ff9b:1::/48` parses successfully, `isValid()` is `true`, and no classifier catches it. Which internal target is reached depends on the NAT64 prefix deployed on the server's network; the well-known prefix column is the patched control from GHSA-22jq-vg5j-6vgg.\n\n| Internal target | Well-known form | Classified | Local-use form (`/48` prefix) | Classified |\n|---|---|---|---|---|\n| `127.0.0.1` | `64:ff9b::7f00:1` | loopback | `64:ff9b:1:7f00:0:100::` | nothing |\n| `10.0.0.1` | `64:ff9b::a00:1` | private | `64:ff9b:1:a00:0:100::` | nothing |\n| `169.254.169.254` | `64:ff9b::a9fe:a9fe` | link-local | `64:ff9b:1:a9fe:a9:fe00::` | nothing |\n| `192.168.1.1` | `64:ff9b::c0a8:101` | private | `64:ff9b:1:c0a8:1:100::` | nothing |\n\n### Reachability\n\nReaching an internal host through one of these addresses requires that the server's network run a NAT64 translator on a prefix inside `64:ff9b:1::/48` and that the attacker guess or know the prefix length. That is the same precondition the well-known prefix carries (a translator on `64:ff9b::/96`), with the additional constraint that the prefix is operator-chosen rather than fixed. The severity reflects that precondition.\n\n### Proof of concept\n\n`npm i ip-address@10.5.0`, then:\n\n```js\nconst { Address6 } = require('ip-address');\n\n// A guard of the shape the library documents.\nfunction isBlocked(host) {\n  const a = new Address6(host);\n  return a.isLoopback() || a.isLinkLocal() || a.isPrivate() || a.isMulticast() || a.isUnspecified();\n}\n\nfor (const h of ['64:ff9b::7f00:1', '64:ff9b:1:7f00:0:100::', '64:ff9b:1::7f00:1']) {\n  console.log(isBlocked(h) ? 'BLOCK' : 'ALLOW', h, '-\u003e getType()', new Address6(h).getType());\n}\n```\n\nOn affected versions:\n\n```\nBLOCK 64:ff9b::7f00:1 -\u003e getType() NAT64 (well-known)\nALLOW 64:ff9b:1:7f00:0:100:: -\u003e getType() NAT64 (local-use)\nALLOW 64:ff9b:1::7f00:1 -\u003e getType() NAT64 (local-use)\n```\n\n### Remediation\n\nUpgrade to the patched release. In the fix, `isPrivate()` returns `true` for every address in `64:ff9b:1::/48`, alongside ULA and the decoded mapped and well-known cases. The range is reported private as a whole rather than by a decoded IPv4 address, for the reason given above; `toAddress4Nat64(prefix)` remains the way to decode an address under a known deployment prefix. `isLoopback()` and `isLinkLocal()` are unchanged for this range, since without the prefix length the library cannot know which IPv4 address is embedded.\n\nIf you cannot upgrade immediately, test the range directly:\n\n```js\nconst NAT64_LOCAL_USE = new Address6('64:ff9b:1::/48');\nconst localUse = new Address6(host).isHostInSubnet(NAT64_LOCAL_USE);\n```\n\n### A note on SSRF defense\n\nThese methods are address classifiers, not a complete SSRF defense. Regardless of this fix, a robust SSRF guard must resolve the hostname and validate the *resolved* IP against the socket it connects to, and account for DNS rebinding and redirects. Treat these checks as one layer, not the only one.","aliases":["CVE-2026-101910"],"modified":"2026-09-28T21:00:05.355682124Z","published":"2026-09-28T20:43:03Z","database_specific":{"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-09-28T20:43:03Z","nvd_published_at":"2026-09-28T18:17:20Z","cwe_ids":["CWE-918"]},"references":[{"type":"WEB","url":"https://github.com/beaugunderson/ip-address/security/advisories/GHSA-2vr4-cq9g-pvrc"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-101910"},{"type":"WEB","url":"https://github.com/beaugunderson/ip-address/commit/ab3dc88bcf5374344168a2ba075ca7ac4ff257f8"},{"type":"PACKAGE","url":"https://github.com/beaugunderson/ip-address"},{"type":"WEB","url":"https://github.com/beaugunderson/ip-address/releases/tag/v10.5.1"}],"affected":[{"package":{"name":"ip-address","ecosystem":"npm","purl":"pkg:npm/ip-address"},"ranges":[{"type":"SEMVER","events":[{"introduced":"10.2.0"},{"fixed":"10.5.1"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 10.5.0","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-2vr4-cq9g-pvrc/GHSA-2vr4-cq9g-pvrc.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N"}]}