diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-08 15:25:22 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-08 15:25:22 +0200 |
| commit | 294009014d74a883112f13dbf48b7403ca79b012 (patch) | |
| tree | 42886672fbace6db95ef1c555facec3813e93212 | |
| parent | 244c36c820b135c39c3d58e10939496b57c845f5 (diff) | |
| download | abusectl-294009014d74a883112f13dbf48b7403ca79b012.tar.gz abusectl-294009014d74a883112f13dbf48b7403ca79b012.zip | |
fix: re-bracket an IPv6 host when stripping URL userinfo
parts.hostname returns an IPv6 literal WITHOUT its brackets
(2001:db8::1, not [2001:db8::1]), and _netloc_without_userinfo
reassembled f"{host}:{port}" directly from it:
redact.url("http://victim@[2001:db8::1]:8080/p?x=1")
-> "http://2001:db8::1:8080/p?x=REDACTED"
That string is not parseable back into a host and a port, and the
digits after the second-to-last colon are not even part of the address
any more. Reporting it means the abuse desk cannot identify the host
at all, or misreads it, which is the same class of harm as reporting a
wrong IP outright.
_netloc_without_userinfo re-adds brackets whenever the hostname
contains ":", so an IPv6 host now survives userinfo stripping exactly
as an IPv4 or named host already did.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
| -rw-r--r-- | abusectl/redact.py | 14 | ||||
| -rw-r--r-- | tests/test_redact.py | 12 |
2 files changed, 24 insertions, 2 deletions
diff --git a/abusectl/redact.py b/abusectl/redact.py index 5e7c4d3..15b1345 100644 --- a/abusectl/redact.py +++ b/abusectl/redact.py @@ -105,8 +105,18 @@ def _redact_fragment(fragment: str) -> str: def _netloc_without_userinfo(parts) -> str: - """Return scheme://host[:port] worth of netloc, dropping any userinfo.""" - netloc = parts.hostname or "" + """Return scheme://host[:port] worth of netloc, dropping any userinfo. + + ``parts.hostname`` returns an IPv6 literal WITHOUT its brackets + (``2001:db8::1``, not ``[2001:db8::1]``), so re-bracketing it here is + required, not cosmetic: without it, appending ``:8080`` produces + ``2001:db8::1:8080``, which no consumer can parse back into a host and + a port, and which is not even the same address any more as text. + """ + host = parts.hostname or "" + if ":" in host: + host = f"[{host}]" + netloc = host if parts.port is not None: netloc = f"{netloc}:{parts.port}" return netloc diff --git a/tests/test_redact.py b/tests/test_redact.py index e6d65c0..553ffec 100644 --- a/tests/test_redact.py +++ b/tests/test_redact.py @@ -65,6 +65,18 @@ class TestRedactUrl(unittest.TestCase): "http://a.invalid/p#REDACTED", ) + def test_an_ipv6_host_survives_userinfo_stripping(self): + self.assertEqual( + redact.url("http://victim@[2001:db8::1]:8080/p?x=1"), + "http://[2001:db8::1]:8080/p?x=REDACTED", + ) + + def test_an_ipv6_host_without_userinfo_is_untouched(self): + self.assertEqual( + redact.url("http://[2001:db8::1]:8080/p?x=1"), + "http://[2001:db8::1]:8080/p?x=REDACTED", + ) + class TestSuspectPathSegments(unittest.TestCase): def test_a_base64_looking_segment_is_flagged(self): |
