diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-08 16:28:01 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-08 16:28:01 +0200 |
| commit | fd8339703ccb99fccb95059996001f6734043655 (patch) | |
| tree | b94d321d11f7559bac3f69a4a6b27d9951a38c88 /tests/test_parse.py | |
| parent | 4d65c2de0a64a663de4458f5cfdc6c59dac31ec6 (diff) | |
| download | abusectl-fd8339703ccb99fccb95059996001f6734043655.tar.gz abusectl-fd8339703ccb99fccb95059996001f6734043655.zip | |
fix: take the sender domain from the address, not the display name
_ADDR_DOMAIN.search() returned the first @domain anywhere in the raw
header text. A display name sits before the angle brackets and is
attacker-controlled, so it won.
Two ways that reached a published report. The sender was misattributed:
"Billing at billing@innocent.example" <phish@sender.example.invalid>
filed the report against a third party who sent nothing. And it defeated
the structural guarantee in sender_domains(): the module reads no
recipient header, but an attacker who writes the victim's own address
into the display name hands it one anyway, and it came back out as a
sender domain.
parseaddr() parses the header grammar rather than scanning it, so a
quoted display name cannot supply the address.
leaky.eml's display name now carries the recipient address. The existing
test_no_ioc_holds_a_recipient_address assertion catches this class; it
was green before only because the fixture used a harmless domain.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NHaqA1Rz5ybed7wFUeQbK
Diffstat (limited to 'tests/test_parse.py')
| -rw-r--r-- | tests/test_parse.py | 7 |
1 files changed, 7 insertions, 0 deletions
diff --git a/tests/test_parse.py b/tests/test_parse.py index 49fc41e..de78b5a 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -108,6 +108,13 @@ class TestSenderDomains(unittest.TestCase): domains = parse.sender_domains(load("simple.eml")) self.assertNotIn("example.org", domains.values()) + def test_the_domain_comes_from_the_address_not_the_display_name(self): + # A display name is attacker-controlled and sits BEFORE the angle + # brackets, so a regex scanning the raw header finds it first. Doing + # that files the report against whoever the attacker named. + domains = parse.sender_domains(load("leaky.eml")) + self.assertEqual(domains["from"], "sender.example.invalid") + class TestAuthResults(unittest.TestCase): def test_verdicts_are_read_as_the_server_recorded_them(self): |
