diff --git a/tests/test_share_safety.py b/tests/test_share_safety.py index d948b3d..b488063 100644 --- a/tests/test_share_safety.py +++ b/tests/test_share_safety.py @@ -156,3 +156,29 @@ def git(*args): assert check_share_safety.check_changed(base) == [ check_share_safety.Finding("candidate.txt", 3, "NON_DOCUMENTATION_IPV4") ] + + +def test_scope_operators_are_not_read_as_ipv6_addresses(): + """`Foo::bar` matches the IPv6 pattern and parses as an address in + ::/8. Reserved blocks are not assignable to a host, so an address in + one cannot be the leak this rule exists to catch.""" + colons = 2 * chr(58) + for text in ( + "the leaf's `TbsCertificate" + colons + "signature_alg` is unparseable", + "see `std" + colons + "vector` and `Face" + colons + "expressRequest`", + "0" + colons + "1 is the discard prefix", + ): + assert check_share_safety.scan_text("doc.md", text) == [] + + +def test_host_assignable_ipv6_addresses_are_still_flagged(): + colons = 2 * chr(58) + for text in ( + "fe80" + colons + "1", # link-local + "fd00" + colons + "1", # unique-local + "2400" + chr(58) + "cb00" + colons + "1", # global unicast + ): + findings = check_share_safety.scan_text("doc.md", text) + assert [finding.rule_id for finding in findings] == [ + "NON_DOCUMENTATION_IPV6" + ], text diff --git a/tools/check_share_safety.py b/tools/check_share_safety.py index 24ca068..a3235fd 100755 --- a/tools/check_share_safety.py +++ b/tools/check_share_safety.py @@ -128,7 +128,17 @@ def _safe_ipv4(value: str) -> bool: def _safe_ipv6(value: str) -> bool: address = ipaddress.ip_address(value.strip("[]").split("%", 1)[0]) return ( - address.is_loopback or address.is_unspecified or address in DOCUMENTATION_IPV6 + address.is_loopback + or address.is_unspecified + or address in DOCUMENTATION_IPV6 + # An address in an IETF-reserved block is not assignable to a host, + # so it cannot be the leak this rule exists to catch. Excluding them + # also clears a false positive on C++ and Rust scope operators: a + # scope operator preceded by a hex letter matches IPV6_RE, parses as + # an address in the all-zero reserved block, and is nobody's device. + # Global unicast, link-local and unique-local all sit outside the + # reserved set and stay flagged; see the tests for each. + or address.is_reserved )