diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-09 09:47:51 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-09 09:47:51 +0200 |
| commit | 745473b0edecc16915f761b15e58b23bc5bd69b9 (patch) | |
| tree | 08565af9a952603047f45e8082fb0d990e996e70 /tests/test_rdap.py | |
| parent | 7fbd109ed8e00ca1184015ba50c30ee92cce1ad8 (diff) | |
| download | abusectl-745473b0edecc16915f761b15e58b23bc5bd69b9.tar.gz abusectl-745473b0edecc16915f761b15e58b23bc5bd69b9.zip | |
fix: keep ":" literal so an IPv6 query is not over-encoded
6d5822b quoted the interpolated component with safe="", which encoded the
colons of every IPv6 address: 2001:db8::1 went on the wire as
2001%3Adb8%3A%3A1.
RFC 9082 specifies the IPv6 object path segment as the address in its text
form, and RFC 3986 pchar permits ":" in a segment, so the colon must stay
literal. The failure mode was the bad one for this tool: a registry that
does not normalise before matching answers 404, which reads here as "this
netblock publishes no abuse desk" rather than "we asked the wrong
question", losing the abuse contact silently on exactly the IPv6 hops the
tool exists to report.
safe=":" keeps everything the original commit was for. "/" is still
encoded, so the traversal containment is unchanged, and "%" is still
encoded, so a scope id stays contained.
The regression got through because the happy-path guard used an IPv4
address only. The new test asserts a documentation-range IPv6 address
round-trips into the URL unencoded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrfqr2xqQfhtXCscU7zrdz
Diffstat (limited to 'tests/test_rdap.py')
| -rw-r--r-- | tests/test_rdap.py | 23 |
1 files changed, 19 insertions, 4 deletions
diff --git a/tests/test_rdap.py b/tests/test_rdap.py index 815c311..1045160 100644 --- a/tests/test_rdap.py +++ b/tests/test_rdap.py @@ -459,6 +459,7 @@ class QuotedQueryComponent(unittest.TestCase): IPV4 = {"services": [[["198.51.100.0/24"], ["https://rir.example.invalid/"]]]} IPV6 = {"services": [[["fe80::/10"], ["https://rir.example.invalid/"]]]} + DOCV6 = {"services": [[["2001:db8::/32"], ["https://r6.example.invalid/"]]]} DNS = {"services": [[["invalid"], ["https://registry.example.invalid/"]]]} def _recorder(self): @@ -493,14 +494,28 @@ class QuotedQueryComponent(unittest.TestCase): """A bare % in a URL is a truncated escape, not a literal percent. server_for_ip accepts fe80::1%eth0 today, so this one reaches the - wire through the normal path. + wire through the normal path. The colons stay literal; only the + scope separator is encoded. """ calls, fetch = self._recorder() rdap.query_ip("fe80::1%eth0", self.IPV6, fetch=fetch) - self.assertEqual( - calls, ["https://rir.example.invalid/ip/fe80%3A%3A1%25eth0"] - ) + self.assertEqual(calls, ["https://rir.example.invalid/ip/fe80::1%25eth0"]) + + def test_a_normal_ipv6_address_is_not_encoded_at_all(self): + """RFC 9082 wants the address in its TEXT form, and pchar allows ":". + + Encoding the colon does not fail loudly. A registry that does not + normalise before matching answers 404, which this tool reads as + "this netblock publishes no abuse desk" rather than "we asked the + wrong question", and the abuse contact for an IPv6 hop is lost + silently. The happy-path guard was IPv4 only, so an over-broad + safe="" got through once already. + """ + calls, fetch = self._recorder() + rdap.query_ip("2001:db8::1", self.DOCV6, fetch=fetch) + + self.assertEqual(calls, ["https://r6.example.invalid/ip/2001:db8::1"]) def test_a_query_and_fragment_in_a_host_are_not_live(self): calls, fetch = self._recorder() |
