From 244c36c820b135c39c3d58e10939496b57c845f5 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 8 Sep 2026 15:24:27 +0200 Subject: fix: redact a valueless query token whole, not as a kept name parse_qsl reads "?victim@example.org" as the pair ("victim@example.org", ""), and the code kept parameter NAMES because they fingerprint the phishing kit. A name that is itself an address is not a name, so keeping it published the recipient's identifier verbatim (percent-decoded, no less: %40 fools no consumer). _redact_kv_string now splits the query/fragment string by hand on "&" and ";" and inspects each token's own name: one containing "@" is a value that landed in name position and is redacted WHOLE ("?REDACTED" rather than "?victim%40example.org=REDACTED"); an ordinary name still keeps its shape ("?flag" stays "?flag=REDACTED", "?t=1&t=2" stays "?t=REDACTED&t=REDACTED"). This also exposed a second leak reachable through the same fixture: parse._suspect_segments() ran redact.suspect_path_segments()'s decode-and-check predicate (meant for a querystring smuggled past percent-encoding into a PATH segment) against a raw query VALUE, so a plaintext "?e=you@example.org" reproduced the address in the manifest's suspect_path_segments flag even though the URL itself was correctly redacted. redact.suspect_path_segments() now exposes the opaque-shape half of its check as _looks_opaque(), and parse.py uses only that half against query values: a query value is always fully redacted regardless, so the flag may hint at its shape but must never reproduce it. Turns tests.test_parse.TestIocAssembly.test_the_address_does_not_survive_any_url_shape green, and closes the gap test_no_ioc_holds_a_recipient_address had been passing over with URL redaction fully disabled. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R --- tests/test_redact.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'tests') diff --git a/tests/test_redact.py b/tests/test_redact.py index 23c2a45..e6d65c0 100644 --- a/tests/test_redact.py +++ b/tests/test_redact.py @@ -51,6 +51,20 @@ class TestRedactUrl(unittest.TestCase): "http://a.example.invalid/p?t=REDACTED&t=REDACTED", ) + def test_a_valueless_token_is_redacted_whole(self): + # parse_qsl reads ?victim@example.org as a NAME, and names are kept. + # A token with no "=" is a value, not a fingerprint. + self.assertEqual( + redact.url("http://a.invalid/p?victim@example.org"), + "http://a.invalid/p?REDACTED", + ) + + def test_a_valueless_token_in_a_fragment_is_redacted_whole(self): + self.assertEqual( + redact.url("http://a.invalid/p#victim@example.org"), + "http://a.invalid/p#REDACTED", + ) + class TestSuspectPathSegments(unittest.TestCase): def test_a_base64_looking_segment_is_flagged(self): -- cgit v1.2.3