GHSA-4xrf-jv44-h6hh — ip-address
Fix: beaugunderson/ip-address@488fe9bGHSA-4xrf-jv44-h6hh is a Improper Input Validation vulnerability in ip-address. A fix is available for ip-address — see the affected versions and patch details below.
ip-address: a CIDR suffix on the parsed address suppresses special-use classification and can bypass SSRF and trust-boundary checks
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for GHSA-4xrf-jv44-h6hh.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Real-World Exposure
How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.
ip-addressnpmDescription
Summary
Every special-use classification method is built on isInSubnet, which short-circuits to false whenever the address's own subnet mask is shorter than the reference range's mask. That mask comes verbatim from the CIDR suffix on the parsed input, so appending a suffix such as /0 suppresses classification entirely: isLoopback(), isPrivate(), isLinkLocal(), isCGNAT(), isMulticast(), isUnspecified(), isBroadcast(), isULA(), and getType() all report an internal address as unremarkable, while correctForm() and address still return the real internal target.
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) may therefore treat 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
isInSubnet in src/common.ts opens with a guard that compares the two prefix lengths:
export function isInSubnet(this, address) {
if (this.subnetMask < address.subnetMask) {
return false; // <-- reached before any bit comparison
}
if (this.mask(address.subnetMask) === address.mask()) {
return true;
}
return false;
}
That guard is correct for the question isInSubnet is named for — whether one network is contained in another, where a /0 network genuinely is not inside a /8. It is wrong for classification, which asks a question about the address itself and must not depend on the prefix the caller happened to write. /0 is shorter than every reference prefix in the special-use tables (loopback /8, link-local /16, CGNAT /10, ULA /7, multicast /4), so for a classification call the bit comparison is never reached and the method returns false.
The underlying bit comparison is correct, and mask(n) already returns the first n bits of the full parsed address independently of subnetMask — the defect is solely that the containment guard sits in the classification path. Host bits are retained through parsing, so correctForm() still yields the real target and the address remains fully usable for connecting.
/0 is the universal case because it is shorter than every reference prefix, but any suffix shorter than the specific range being tested has the same effect: 10.0.0.5/7 defeats isPrivate() for 10.0.0.0/8.
Affected versions
>= 10.1.1, <= 10.2.1. The is* classification API was introduced for Address4 in 10.1.1 and extended to Address6 in 10.2.0; releases before 10.1.1 do not expose it and are not affected through this vector. The containment guard itself is much older, but isInSubnet alone is a subnet-containment predicate whose behavior here is correct.
This also defeats the fix released in 10.2.1 for GHSA-22jq-vg5j-6vgg: that release classifies IPv4-mapped and NAT64 addresses by their embedded IPv4 address, but the normalization is reached through isInSubnet, so ::ffff:127.0.0.1/0 reverts to being reported as non-internal.
Impact
Every classifier is affected on both Address4 and Address6. The sole exception is Address6.isLinkLocal() for native fe80::/10 addresses, which compares raw bits directly; its IPv4-mapped path is still affected.
| Address | Reported as | Actually points at |
|---|---|---|
127.0.0.1/0 | not loopback | loopback (127.0.0.0/8) |
10.0.0.1/0, 10.0.0.5/7 | not private | RFC 1918 10/8 |
172.16.5.5/0 | not private | RFC 1918 172.16/12 |
192.168.1.1/0 | not private | RFC 1918 192.168/16 |
169.254.169.254/0 | not link-local | link-local / cloud metadata (IMDS) |
100.64.0.1/0 | not CGNAT | CGNAT 100.64/10 |
0.0.0.0/0, 255.255.255.255/0 | not unspecified / not broadcast | unspecified / broadcast |
::1/0 | not loopback | IPv6 loopback |
fc00::1/0 | not ULA, not private | IPv6 ULA fc00::/7 |
ff02::1/0 | not multicast | IPv6 multicast |
::ffff:127.0.0.1/0 | not loopback | loopback, via IPv4-mapped |
::ffff:169.254.169.254/0 | not link-local | IMDS, via IPv4-mapped |
64:ff9b::7f00:1/0 | not loopback | loopback, via NAT64 |
getType() returns Global unicast for all of the IPv6 cases above, and getScope() follows it.
Reachability
A CIDR suffix is not legal in a URL host, so this is not reachable through the most common SSRF shape. new URL('http://127.0.0.1/0') parses hostname as 127.0.0.1 and pathname as /0, and a guard that classifies the extracted hostname is unaffected. Exploitation requires an application that accepts a bare address string that may carry a suffix and passes it to the constructor before classifying — for example an allow/deny field, a webhook target, or a proxy destination taken as a plain host rather than parsed out of a URL.
Proof of concept
npm i [email protected], then:
const { Address4, Address6 } = require('ip-address');
// true => block as internal, false => allow outbound
function isBlocked(host) {
try {
const a = new Address4(host);
return a.isPrivate() || a.isLoopback() || a.isLinkLocal() || a.isCGNAT()
|| a.isMulticast() || a.isUnspecified() || a.isBroadcast();
} catch {}
try {
const a = new Address6(host);
return a.isPrivate() || a.isLoopback() || a.isLinkLocal() || a.isULA()
|| a.isMulticast() || a.isUnspecified();
} catch {}
return false;
}
for (const h of ['127.0.0.1', '10.0.0.1', '::1',
'127.0.0.1/0', '10.0.0.5/7', '169.254.169.254/0',
'::1/0', '::ffff:127.0.0.1/0', '64:ff9b::7f00:1/0']) {
console.log(isBlocked(h) ? 'BLOCK ' : 'ALLOW ', h, '->', new (h.includes(':') ? Address6 : Address4)(h).correctForm());
}
On affected versions every suffixed internal target is allowed, and correctForm() shows the request would reach the real internal address:
BLOCK 127.0.0.1 -> 127.0.0.1
BLOCK 10.0.0.1 -> 10.0.0.1
BLOCK ::1 -> ::1
ALLOW 127.0.0.1/0 -> 127.0.0.1
ALLOW 10.0.0.5/7 -> 10.0.0.5
ALLOW 169.254.169.254/0 -> 169.254.169.254
ALLOW ::1/0 -> ::1
ALLOW ::ffff:127.0.0.1/0 -> ::ffff:7f00:1
ALLOW 64:ff9b::7f00:1/0 -> 64:ff9b::7f00:1
The first three lines are blocked as expected; the same destinations with a CIDR suffix are allowed through.
Remediation
Upgrade to the patched release. In the fix, classification no longer consults the address's own prefix: a new isHostInSubnet() compares the address's host bits against the reference range only, and every classifier (isLoopback, isPrivate, isLinkLocal, isCGNAT, isMulticast, isUnspecified, isBroadcast, isULA, isMapped4, isTeredo, is6to4, isDocumentation, getType, and the IPv4-mapped/NAT64 normalization behind embeddedIPv4) uses it. isInSubnet keeps its subnet-containment semantics unchanged, including the guard that a wider network is not contained in a narrower one. After upgrading, new Address4('127.0.0.1/0').isLoopback() returns true.
If you cannot upgrade immediately, strip the suffix before classifying by re-parsing addressMinusSuffix:
const parsed = new Address4(userInput);
const host = new Address4(parsed.addressMinusSuffix); // classify this one
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.
Credit
Reported by @hi-im-glitchless.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 📦npm | ip-address | ≥ 10.1.1&&< 10.2.2 | 10.2.2npm install ip-address@10.2.2 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for ip-address, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update ip-address to 10.2.2 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-4xrf-jv44-h6hh is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-4xrf-jv44-h6hh can be triaged on real exposure rather than presence alone.
Tailored to GHSA-4xrf-jv44-h6hh. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Fixing This On Your OS
If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.
Red Hat products that ship the affected ip-address library versions use it in browser-side UI components for IP address display and form validation, not for server-side request filtering or SSRF protection. The classification bypass has no security-relevant effect in this context because no trust-boundary decisions…
| Product | Fixed in | Advisory |
|---|---|---|
| Red Hat Hardened Images | grafana12-4-main-12.4.6-0.3.hum1 | RHSA-2026:49401 |
| Red Hat Hardened Images | grafana13-1-main-13.1.1-0.5.2.hum1 | RHSA-2026:50826 |
Frequently Asked Questions
Is GHSA-4xrf-jv44-h6hh in your dependencies?
O3 Security finds GHSA-4xrf-jv44-h6hh across npm dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.