diff options
Diffstat (limited to 'tests/test_report.py')
| -rw-r--r-- | tests/test_report.py | 41 |
1 files changed, 40 insertions, 1 deletions
diff --git a/tests/test_report.py b/tests/test_report.py index 9159dc7..9a19d00 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -8,7 +8,7 @@ import tempfile import unittest from pathlib import Path -from abusectl import report +from abusectl import parse, report class Grouping(unittest.TestCase): @@ -1958,3 +1958,42 @@ class Writing(unittest.TestCase): if __name__ == "__main__": unittest.main() + + +class PersonalisedSubject(unittest.TestCase): + """The documented limit, pinned so a later change has to face it. + + A phishing kit personalises the Subject with the recipient's local part. + The whitelist governs WHICH headers travel, never what is inside one, so + that local part is published. The spec accepts this deliberately: Subject + is what lets a desk recognise a campaign, and filtering free text is the + judgement-shaped problem AGENTS.md names as the origin of every leak this + project has had. + + Measured, not theoretical: a sweep of 92 real messages found 14 carrying + the recipient's local part in the Subject, and none in the From display + name. This test exists so that number cannot change silently. + """ + + def _headers(self): + raw = (Path(__file__).parent + / "fixtures" / "personalised-subject.eml").read_bytes() + return dict(parse.report_headers(raw, trusted=["192.0.2.0/24"])) + + def test_the_envelope_recipient_is_cut_from_the_received_line(self): + # The protection that DOES hold: our own relay wrote the "for" + # clause, and it is cut before the line is ever stored. + received = self._headers()["Received"] + self.assertNotIn("alicejones", received) + self.assertNotIn("@example.org", received) + + def test_no_published_header_carries_the_full_recipient_address(self): + published = " ".join(self._headers().values()) + self.assertNotIn("alicejones@example.org", published) + + def test_the_subject_still_carries_the_local_part(self): + # Deliberate. If this ever fails because Subject was filtered, the + # spec's "considered and not built" section is what to read first: + # the fix is a redaction rule with its own tests, not a passthrough + # quietly turned into a filter. + self.assertIn("alicejones", self._headers()["Subject"]) |
