Add isGlobal() and pin the classifiers to the IANA special-purpose registries - #224
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'sipaddressfound eight ranges no classifier caught:0.0.0.0/8, the IETF protocol assignments in192.0.0.0/24and2001::/23, the three IPv4 documentation blocks,198.18.0.0/15,240.0.0.0/4,100::/64and100:0:0:1::/64, and3fff::/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 insrc/v4/constants.ts/src/v6/constants.tsthat mirrors the registry row for row (block, name, Globally Reachable). The most specific containing entry with an answer wins, so192.0.0.9(PCP anycast, reachable) is global inside the non-reachable192.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 atif (!address.isGlobal()) reject().Parity methods:
Address4.isDocumentation(),isBenchmarking(),isReserved();Address6.isDocumentation()covers3fff::/20(RFC 9637) andAddress6.isBenchmarking()is new;getType()learnsBenchmarking,Discard-onlyand the secondDocumentationblock.Registry-pinned test:
test/data/iana-corpus.json(592 probes) is generated byscripts/gen-iana-corpus.py(stdlib only;--fetchrefreshes the CSVs from iana.org) from the registry CSVs checked in beside it.test/iana-corpus-test.tsassertsisGlobal()matches the registry on every probe and thatisLoopback/isLinkLocal/isMulticast/isUnspecifiedmatch 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-localfec0::/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 only2000::/3for global unicast.Address6.isGlobal()is now gated on2000::/3, which also covers the deprecated IPv4-compatible::/96and unallocated space like4000::/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.