From 75d9744a53acf091e8a2a152210647773db189d1 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 9 Sep 2026 19:07:08 +0200 Subject: fix: strip the envelope recipient in every shape the grammar allows The for-clause rule anchored on the clause terminator, which is strictly stronger than RFC 5321 4.4: For sits inside Opt-info, so With, ID, Via or a CFWS comment may legitimately follow it, and its ABNF is 1*( Path / Mailbox ) where Mailbox carries no angle brackets. Only the tidiest shape was stripped. Four ordinary ones published the victim's address in the one header a report reproduces verbatim, including "for (envelope-from );", which is routine Exim and Sendmail output. Anchored on the address instead. The envelope SENDER survives the cut, since that is what the report is about, and the leftover whitespace, orphan separator and emptied comment are tidied because the line is published to a third party verbatim. Table-driven over all five shapes; reverting the regex fails six of nine tests in the class. Two accepted disclosures are now named in the spec's "Where the headers come from" rather than living in a test comment: our own relay's hostname, which the by and authserv-id clauses carry by construction, and attacker-controlled free text in Subject and the From display name, which the whitelist does not and should not filter. The second is asserted as a documented limit so it reads as deliberate, and the manifest assertion now bars the obfuscated you%40example.org spelling alongside the plain one. case.create() seeds an empty headers block so an unparsed case has the same shape as a parsed one. Additive, so FORMAT_VERSION is unchanged. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Xj1ayFRSUQ2u7cwb3S4axE --- tests/test_case.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'tests/test_case.py') diff --git a/tests/test_case.py b/tests/test_case.py index c6964a9..ab7eeeb 100644 --- a/tests/test_case.py +++ b/tests/test_case.py @@ -45,6 +45,16 @@ class TestCaseCreation(unittest.TestCase): manifest = json.loads((created.path / "manifest.json").read_text()) self.assertEqual(manifest["format"], case.FORMAT_VERSION) + def test_every_block_is_seeded_present_and_empty(self): + # A created-but-unparsed case must have the same SHAPE as a parsed + # one, so a later reader indexes a block rather than guarding every + # access. report will read headers and would hit a KeyError. + created = case.create(self.root, b"x") + manifest = json.loads((created.path / "manifest.json").read_text()) + for block in ("iocs", "auth", "contacts", "destinations", "headers"): + self.assertIn(block, manifest) + self.assertEqual(manifest[block], []) + def test_two_cases_do_not_collide(self): a = case.create(self.root, b"one") b = case.create(self.root, b"two") -- cgit v1.2.3