Skip to content

Add isGlobal() and pin the classifiers to the IANA special-purpose registries - #224

Merged
beaugunderson merged 2 commits into
mainfrom
bg-is-global
Aug 29, 2026
Merged

Add isGlobal() and pin the classifiers to the IANA special-purpose registries#224
beaugunderson merged 2 commits into
mainfrom
bg-is-global

Conversation

@beaugunderson

@beaugunderson beaugunderson commented Aug 29, 2026

Copy link
Copy Markdown
Owner

A guard written as an OR of named classifiers (isPrivate() || isLoopback() || isLinkLocal() || …) covers only the ranges it names, and the IANA special-purpose registries hold more ranges than there are names. A differential sweep of every registry block boundary (plus the IPv4-mapped and NAT64 forms of each) against Python's ipaddress found eight ranges no classifier caught: 0.0.0.0/8, the IETF protocol assignments in 192.0.0.0/24 and 2001::/23, the three IPv4 documentation blocks, 198.18.0.0/15, 240.0.0.0/4, 100::/64 and 100:0:0:1::/64, and 3fff::/20. Each is the shape of the last four advisories: a well-formed literal that parses and classifies as unremarkable.

isGlobal() on both classes answers from a table in src/v4/constants.ts / src/v6/constants.ts that mirrors the registry row for row (block, name, Globally Reachable). The most specific containing entry with an answer wins, so 192.0.0.9 (PCP anycast, reachable) is global inside the non-reachable 192.0.0.0/24. Multicast is not global; Teredo and 6to4 (registry N/A) are not global; IPv4-mapped and NAT64 well-known forms answer for the embedded IPv4 address. SECURITY.md and the README now point guards at if (!address.isGlobal()) reject().

Parity methods: Address4.isDocumentation(), isBenchmarking(), isReserved(); Address6.isDocumentation() covers 3fff::/20 (RFC 9637) and Address6.isBenchmarking() is new; getType() learns Benchmarking, Discard-only and the second Documentation block.

Registry-pinned test: test/data/iana-corpus.json (592 probes) is generated by scripts/gen-iana-corpus.py (stdlib only; --fetch refreshes the CSVs from iana.org) from the registry CSVs checked in beside it. test/iana-corpus-test.ts asserts isGlobal() matches the registry on every probe and that isLoopback/isLinkLocal/isMulticast/isUnspecified match Python. Python 3.14 and the registry disagree on four rows (192.88.99.2, 100:0:0:1::/64, 2001:1::3, 5f00::/16); those are listed with a tripwire that fails once Python catches up. A registry change becomes a failing test after a regen instead of an inbound advisory.

Second pass, from a survey of recent advisories. Pulled every GitHub Advisory Database entry for the IP-parsing libraries across ecosystems plus every CWE-918 advisory since 2022 that mentions an address form (986 total), harvested the 525 distinct address literals they cite, and ran them through the library against Python. Every ip-CVE form (0x7F.1, 127.1, 01200034567, 012.1.2.3, 0177.0.0.1, decimal, trailing dot, whitespace, fullwidth digits) is rejected; ::fFFf:127.0.0.1, 000:0:0000::01, ::, 6to4/Teredo-embedded internals, and NAT64 local-use forms all classify as not global. The one gap was the deprecated site-local fec0::/10, which GHSA-w98g-5w9p-p3rc (2026-08-28) names as a deny-list miss: the special-purpose registry lists exceptions, but the IANA IPv6 Address Space Registry allocates only 2000::/3 for global unicast. Address6.isGlobal() is now gated on 2000::/3, which also covers the deprecated IPv4-compatible ::/96 and unallocated space like 4000::/3; the address-space CSV joins the fixtures and its boundaries join the corpus (730 probes). Python reports those reserved ranges as global, so the Python cross-check skips them with a comment.

…gistries

A guard written as an OR of named classifiers covers only the ranges it
names, and the IANA registries hold more ranges than there are names. A
differential sweep of every registry block boundary against Python's
ipaddress found eight ranges no classifier caught: 0.0.0.0/8, the IETF
protocol assignments in 192.0.0.0/24 and 2001::/23, the three IPv4
documentation blocks, 198.18.0.0/15, 240.0.0.0/4, 100::/64 and
100:0:0:1::/64, and 3fff::/20.

isGlobal() on both classes answers from a table that mirrors the
registry row for row, taking the most specific entry's Globally
Reachable column, treating multicast as not global, and answering for
the embedded IPv4 address of mapped and NAT64 well-known forms. SECURITY
and the README point guards at it.

For parity with the IPv6 side, Address4 gains isDocumentation(),
isBenchmarking() and isReserved(); Address6.isDocumentation() covers
3fff::/20 and Address6 gains isBenchmarking(); getType() learns
Benchmarking, Discard-only and the second Documentation block.

test/data/iana-corpus.json is generated by scripts/gen-iana-corpus.py
from the registry CSVs checked in beside it: every block's first, last,
neighboring and middle addresses plus their mapped and NAT64 forms, each
carrying the registry's answer and Python's. The suite asserts isGlobal()
matches the registry on all 592 and the named classifiers match Python,
with the four rows where Python 3.14 and the registry disagree listed as
tripwires.
The special-purpose registry lists exceptions, but the IANA IPv6 Address
Space Registry says only 2000::/3 is allocated for global unicast at all,
so the deprecated site-local fec0::/10, the deprecated IPv4-compatible
::/96, and unallocated space such as 4000::/3 have nowhere to route.
isGlobal() answered true for all of them; a survey of recent SSRF
advisories found fec0::/10 named as a deny-list gap (GHSA-w98g-5w9p-p3rc).

The address-space registry CSV joins the fixtures and its block
boundaries join the corpus (730 probes). getType() names the two
deprecated ranges.
@beaugunderson
beaugunderson merged commit fb12583 into main Aug 29, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant