From c734feaf769c936698a52f38a2099152a6098c43 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 8 Sep 2026 13:32:20 +0200 Subject: fix: redact fragment and strip userinfo, flag hidden query in path The query string was not the only place a recipient identifier can hide. A fragment (#e=victim@...) is published as-is since we report the URL's literal text, not what a browser would send. Userinfo (user:pass@host) leaks a credential as well as an identifier, so it is stripped outright rather than redacted in place. A path segment can also smuggle an encoded query (%3Fe=victim@...); suspect_path_segments now flags a segment that decodes to something containing '=' or '@', still leaving the decision to redact or not to human review. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R --- tests/test_redact.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) (limited to 'tests') diff --git a/tests/test_redact.py b/tests/test_redact.py index 91ab291..23c2a45 100644 --- a/tests/test_redact.py +++ b/tests/test_redact.py @@ -102,5 +102,47 @@ class TestUrlValuedParameters(unittest.TestCase): ) +class TestLeakResistance(unittest.TestCase): + def test_a_fragment_key_value_pair_is_redacted(self): + out = redact.url("http://a.invalid/p?x=1#e=victim@example.org") + self.assertNotIn("victim@example.org", out) + + def test_userinfo_is_stripped_not_redacted_in_place(self): + out = redact.url("http://victim%40example.org:pw@a.invalid/p") + self.assertNotIn("victim%40example.org", out) + self.assertNotIn("pw", out) + + def test_a_query_hidden_inside_a_path_segment_is_flagged(self): + found = redact.suspect_path_segments( + "http://a.invalid/p%3Fe=victim@example.org" + ) + self.assertNotEqual(found, []) + + def test_no_recipient_marker_survives_any_placement(self): + # The same address, placed everywhere a URL can hide one. + marker = "victim@example.org" + encoded = "victim%40example.org" + for raw in ( + f"http://a.invalid/p?e={marker}", + f"http://a.invalid/p?x=1#e={marker}", + f"http://{encoded}:pw@a.invalid/p", + f"http://a.invalid/p?a=1&b=2#{marker}", + ): + with self.subTest(raw=raw): + out = redact.url(raw) + self.assertNotIn(marker, out) + self.assertNotIn(encoded, out) + + +class TestHasUserinfo(unittest.TestCase): + def test_userinfo_present_is_reported(self): + self.assertTrue( + redact.has_userinfo("http://victim%40example.org:pw@a.invalid/p") + ) + + def test_no_userinfo_is_reported_absent(self): + self.assertFalse(redact.has_userinfo("http://a.invalid/p")) + + if __name__ == "__main__": unittest.main() -- cgit v1.2.3