diff --git a/Changes.md b/Changes.md index e09ba5e..0b82ab3 100644 --- a/Changes.md +++ b/Changes.md @@ -1,3 +1,8 @@ +### Unreleased + +- classify IPv4-Compatible IPv6 addresses (RFC4291 §2.5.5.1, `::/96` excluding `::` and `::1`) as `ipv4Compat` instead of `unicast`, so consumers using `range() !== 'unicast'` as an SSRF safety check block `::7f00:1`, `::a9fe:a9fe`, etc. — the pure-hex form that WHATWG URL parsers (`new URL('https://[::127.0.0.1]/').hostname` → `[::7f00:1]`) emit for IPv4-Compatible literals. The more-specific `::/128` and `::1/128` ranges retain their existing `unspecified` and `loopback` classifications. + + ### 2.4.0 - 2026-05-03 - remove Bower support diff --git a/lib/ipaddr.d.ts b/lib/ipaddr.d.ts index ee02c02..d62aca8 100644 --- a/lib/ipaddr.d.ts +++ b/lib/ipaddr.d.ts @@ -1,7 +1,7 @@ declare module "ipaddr.js" { type IPvXRangeDefaults = 'unicast' | 'unspecified' | 'multicast' | 'linkLocal' | 'loopback' | 'reserved' | 'benchmarking' | 'amt'; type IPv4Range = IPvXRangeDefaults | 'broadcast' | 'carrierGradeNat' | 'private' | 'as112'; - type IPv6Range = IPvXRangeDefaults | 'uniqueLocal' | 'ipv4Mapped' | 'rfc6145' | 'rfc6052' | '6to4' | 'teredo' | 'as112v6' | 'orchid2' | 'droneRemoteIdProtocolEntityTags'; + type IPv6Range = IPvXRangeDefaults | 'uniqueLocal' | 'ipv4Mapped' | 'ipv4Compat' | 'rfc6145' | 'rfc6052' | '6to4' | 'teredo' | 'as112v6' | 'orchid2' | 'droneRemoteIdProtocolEntityTags'; interface RangeList { [name: string]: [T, number] | [T, number][]; diff --git a/lib/ipaddr.js b/lib/ipaddr.js index 3b1d449..c3a8e43 100644 --- a/lib/ipaddr.js +++ b/lib/ipaddr.js @@ -567,6 +567,13 @@ loopback: [new IPv6([0, 0, 0, 0, 0, 0, 0, 1]), 128], uniqueLocal: [new IPv6([0xfc00, 0, 0, 0, 0, 0, 0, 0]), 7], ipv4Mapped: [new IPv6([0, 0, 0, 0, 0, 0xffff, 0, 0]), 96], + // RFC4291 Section 2.5.5.1 ("IPv4-Compatible IPv6 Address"). Deprecated + // but still classified as a non-unicast range so consumers using + // range() !== 'unicast' as a safety check (e.g. SSRF filters) treat + // ::a.b.c.d the same way they treat ::ffff:a.b.c.d. Placed after + // unspecified (::/128) and loopback (::1/128) so those keep their + // more-specific classifications. + ipv4Compat: [new IPv6([0, 0, 0, 0, 0, 0, 0, 0]), 96], // RFC3879 deprecatedSiteLocal: [new IPv6([0xfec0, 0, 0, 0, 0, 0, 0, 0]), 10], // RFC6666 diff --git a/test/ipaddr.test.js b/test/ipaddr.test.js index c666522..15ca8bc 100644 --- a/test/ipaddr.test.js +++ b/test/ipaddr.test.js @@ -420,6 +420,23 @@ describe('ipaddr', () => { assert.equal(ipaddr.IPv6.parse('100::42').range(), 'discard'); assert.equal(ipaddr.IPv6.parse('fc00::').range(), 'uniqueLocal'); assert.equal(ipaddr.IPv6.parse('::ffff:192.168.1.10').range(), 'ipv4Mapped'); + // RFC4291 §2.5.5.1 IPv4-Compatible IPv6 addresses (::/96, excluding :: + // and ::1). Deprecated but must not be reported as plain unicast, since + // dual-stack hosts may route them to the embedded IPv4 destination. Note + // that IPv6.parse() rewrites dotted-quad forms ("::127.0.0.1") into the + // mapped form ("::ffff:7f00:1"); the realistic input shape for this + // range is therefore pure-hex, which is also what WHATWG URL parsers + // produce for the IPv4-compatible literal. + assert.equal(ipaddr.IPv6.parse('::7f00:1').range(), 'ipv4Compat'); + assert.equal(ipaddr.IPv6.parse('0:0:0:0:0:0:7f00:1').range(), 'ipv4Compat'); + assert.equal(ipaddr.IPv6.parse('::a00:1').range(), 'ipv4Compat'); + assert.equal(ipaddr.IPv6.parse('::a9fe:a9fe').range(), 'ipv4Compat'); + assert.equal(ipaddr.IPv6.parse('::808:808').range(), 'ipv4Compat'); + // The more-specific ::/128 and ::1/128 ranges keep their existing labels. + // (Asserted above; restated here as a regression guard against the new + // ::/96 entry shadowing them.) + assert.equal(ipaddr.IPv6.parse('::').range(), 'unspecified'); + assert.equal(ipaddr.IPv6.parse('::1').range(), 'loopback'); assert.equal(ipaddr.IPv6.parse('fec0::1234').range(), 'deprecatedSiteLocal'); assert.equal(ipaddr.IPv6.parse('::ffff:0:192.168.1.10').range(), 'rfc6145'); assert.equal(ipaddr.IPv6.parse('64:ff9b::1234').range(), 'rfc6052');