GHSA-2VR4-CQ9G-PVRC

Vulnerability from github – Published: 2026-09-28 20:43 – Updated: 2026-09-28 20:43
VLAI
Summary
ip-address: no classifier recognizes the NAT64 local-use range 64:ff9b:1::/48, allowing SSRF and trust-boundary bypass
Details

Summary

No 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.

An 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.

Details

The 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.

The 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.

What 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.

Affected versions

>= 10.2.0, <= 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.

Impact

Every 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.

Internal target Well-known form Classified Local-use form (/48 prefix) Classified
127.0.0.1 64:ff9b::7f00:1 loopback 64:ff9b:1:7f00:0:100:: nothing
10.0.0.1 64:ff9b::a00:1 private 64:ff9b:1:a00:0:100:: nothing
169.254.169.254 64:ff9b::a9fe:a9fe link-local 64:ff9b:1:a9fe:a9:fe00:: nothing
192.168.1.1 64:ff9b::c0a8:101 private 64:ff9b:1:c0a8:1:100:: nothing

Reachability

Reaching 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.

Proof of concept

npm i ip-address@10.5.0, then:

const { Address6 } = require('ip-address');

// A guard of the shape the library documents.
function isBlocked(host) {
  const a = new Address6(host);
  return a.isLoopback() || a.isLinkLocal() || a.isPrivate() || a.isMulticast() || a.isUnspecified();
}

for (const h of ['64:ff9b::7f00:1', '64:ff9b:1:7f00:0:100::', '64:ff9b:1::7f00:1']) {
  console.log(isBlocked(h) ? 'BLOCK' : 'ALLOW', h, '-> getType()', new Address6(h).getType());
}

On affected versions:

BLOCK 64:ff9b::7f00:1 -> getType() NAT64 (well-known)
ALLOW 64:ff9b:1:7f00:0:100:: -> getType() NAT64 (local-use)
ALLOW 64:ff9b:1::7f00:1 -> getType() NAT64 (local-use)

Remediation

Upgrade 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.

If you cannot upgrade immediately, test the range directly:

const NAT64_LOCAL_USE = new Address6('64:ff9b:1::/48');
const localUse = new Address6(host).isHostInSubnet(NAT64_LOCAL_USE);

A note on SSRF defense

These 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.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 10.5.0"
      },
      "package": {
        "ecosystem": "npm",
        "name": "ip-address"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "10.2.0"
            },
            {
              "fixed": "10.5.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-101910"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-28T20:43:03Z",
    "nvd_published_at": "2026-09-28T18:17:20Z",
    "severity": "MODERATE"
  },
  "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 `\u0027NAT64 (local-use)\u0027`, 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\u0027s `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\u0027s 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\u0027s 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(\u0027ip-address\u0027);\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 [\u002764:ff9b::7f00:1\u0027, \u002764:ff9b:1:7f00:0:100::\u0027, \u002764:ff9b:1::7f00:1\u0027]) {\n  console.log(isBlocked(h) ? \u0027BLOCK\u0027 : \u0027ALLOW\u0027, h, \u0027-\u003e getType()\u0027, 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(\u002764:ff9b:1::/48\u0027);\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.",
  "id": "GHSA-2vr4-cq9g-pvrc",
  "modified": "2026-09-28T20:43:03Z",
  "published": "2026-09-28T20:43:03Z",
  "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "ip-address: no classifier recognizes the NAT64 local-use range 64:ff9b:1::/48, allowing SSRF and trust-boundary bypass"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…