diff --git a/crates/traits/src/app_components/host_safety.rs b/crates/traits/src/app_components/host_safety.rs index b4cc701b..36736bbe 100644 --- a/crates/traits/src/app_components/host_safety.rs +++ b/crates/traits/src/app_components/host_safety.rs @@ -109,9 +109,10 @@ pub fn is_public_ipv6(addr: Ipv6Addr) -> bool { if first == 0x2001 && second == 0x0db8 { return false; } - // Documentation 3fff::/20 (RFC 9637). It falls inside global-unicast + // Documentation 3fff::/20 (RFC 9637): first hextet 0x3fff with the top + // nibble of the second hextet zero. It falls inside global-unicast // 2000::/3, so the terminal rule below would otherwise accept it. - if (first & 0xfff0) == 0x3ff0 { + if first == 0x3fff && (second & 0xf000) == 0 { return false; } // Only global unicast 2000::/3 is routable today; reject anything else not diff --git a/crates/traits/src/app_components/tests.rs b/crates/traits/src/app_components/tests.rs index ae46e1e0..92d12746 100644 --- a/crates/traits/src/app_components/tests.rs +++ b/crates/traits/src/app_components/tests.rs @@ -350,7 +350,7 @@ fn group_avatar_url_rejects_documentation_ipv6_ranges() { "https://[2001:db8::1]/a.png", "https://[2001:db8:abcd:12::1]/a.png", "https://[3fff::1]/a.png", - "https://[3fff:ffff::1]/a.png", + "https://[3fff:0fff::1]/a.png", ] { assert!( validate_and_normalize_group_avatar_url(raw).is_err(), diff --git a/crates/traits/tests/host_safety_public.rs b/crates/traits/tests/host_safety_public.rs index a04b2e38..7e13a920 100644 --- a/crates/traits/tests/host_safety_public.rs +++ b/crates/traits/tests/host_safety_public.rs @@ -80,6 +80,15 @@ fn shared_host_safety_classifies_canonical_ipv6_ranges() { assert!(is_public_ipv6("::ffff:93.184.216.34".parse().unwrap())); assert!(is_public_ip(IpAddr::V6("2606:4700::1".parse().unwrap()))); + // Only the actual RFC 9637 block 3fff:0000::/20 is documentation space. + // Neighbouring global-unicast hextets (3ff0:: to 3ffe::) and 3fff:: past the + // /20 boundary (second hextet >= 0x1000) are ordinary routable space. + for raw in ["3ff0::1", "3ffe::1", "3fff:1000::1", "3fff:ffff::1"] { + let addr: Ipv6Addr = raw.parse().expect("test IPv6 literal parses"); + assert!(is_public_ipv6(addr), "{raw} should be accepted"); + assert!(is_public_ip(IpAddr::V6(addr)), "{raw} should be accepted"); + } + for raw in [ "::1", // loopback "::", // unspecified @@ -91,7 +100,8 @@ fn shared_host_safety_classifies_canonical_ipv6_ranges() { "2002::1", // 6to4 transition prefix "2001::1", // Teredo 2001:0000::/32 "2001:db8::1", // documentation - "3fff::1", // documentation 3fff::/20 + "3fff::1", // documentation 3fff::/20 (lower edge) + "3fff:0fff::1", // documentation 3fff::/20 (upper edge) "4000::1", // outside global-unicast 2000::/3 ] { let addr: Ipv6Addr = raw.parse().expect("test IPv6 literal parses");