diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-08 16:28:35 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-08 16:28:35 +0200 |
| commit | cc855e388dc2c1e02447578d05150c9cff63222f (patch) | |
| tree | 86c2f295606843864b7e886a39f010d89b6e3d73 /tests | |
| parent | 92dba06905ded925bc78e4bac74989363aad62d7 (diff) | |
| download | abusectl-cc855e388dc2c1e02447578d05150c9cff63222f.tar.gz abusectl-cc855e388dc2c1e02447578d05150c9cff63222f.zip | |
feat: report List-Unsubscribe urls and a differing Sender
A sweep of the user's real spam found List-Unsubscribe naming a domain
that appeared nowhere else in the message. It is attacker infrastructure
and was going unreported.
Every url from that header goes through redact.url() like a body url: an
unsubscribe link has to say who is unsubscribing, which makes it one of
the likeliest carriers of a recipient token. mailto: entries are skipped
rather than redacted, since the address is the whole value and nothing
useful survives removing it.
Sender is collected on the same terms as Reply-To, included only when it
differs from From. One repeating From is noise; one naming a separate
relay is the infrastructure behind the run.
Also drops the unused urlencode import left in redact.py when
_redact_kv_string stopped using urllib to rebuild the query string.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NHaqA1Rz5ybed7wFUeQbK
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/fixtures/leaky.eml | 2 | ||||
| -rw-r--r-- | tests/test_parse.py | 17 |
2 files changed, 19 insertions, 0 deletions
diff --git a/tests/fixtures/leaky.eml b/tests/fixtures/leaky.eml index f98d191..5ce3366 100644 --- a/tests/fixtures/leaky.eml +++ b/tests/fixtures/leaky.eml @@ -7,6 +7,8 @@ Received: from sender.example.invalid (unknown [203.0.113.42]) Return-Path: <bounce@sender.example.invalid> From: "Billing at you@example.org" <phish@sender.example.invalid> To: <you@example.org> +Sender: envelope@relay.example.invalid +List-Unsubscribe: <http://unsub.example.invalid/u?e=you@example.org>, <mailto:leave@unsub.example.invalid> Subject: Confirm now Message-ID: <eee555@sender.example.invalid> Date: Tue, 8 Sep 2026 16:00:00 +0200 diff --git a/tests/test_parse.py b/tests/test_parse.py index 60c1fb5..11345c7 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -98,6 +98,12 @@ class TestSenderDomains(unittest.TestCase): }, ) + def test_sender_is_collected_when_it_differs_from_from(self): + # Sender names the party who actually injected the message, which on + # a spam run is often a relay distinct from the forged From. + domains = parse.sender_domains(load("leaky.eml")) + self.assertEqual(domains["sender"], "relay.example.invalid") + def test_reply_to_is_absent_when_it_matches_from(self): # Only a DIFFERING Reply-To is an indicator; repeating From adds noise. domains = parse.sender_domains(load("with-attachment.eml")) @@ -245,6 +251,17 @@ class TestIocAssembly(unittest.TestCase): self.assertEqual(targets[0]["value"], "http://evil.example.invalid/pay?ref=REDACTED") + def test_an_unsubscribe_url_is_reported_and_redacted(self): + # List-Unsubscribe routinely names a domain appearing nowhere else, + # and an unsubscribe link is a prime carrier of a recipient token, + # so it is an indicator that must arrive redacted. + iocs = parse.iocs(load("leaky.eml"), trusted=["192.0.2.0/24"]) + unsub = [i for i in iocs if i["origin"] == "header-list_unsubscribe"] + self.assertEqual( + [i["value"] for i in unsub], + ["http://unsub.example.invalid/u?e=REDACTED"], + ) + def test_an_attachment_becomes_a_hash_ioc(self): iocs = parse.iocs(load("with-attachment.eml"), trusted=["192.0.2.0/24"]) hashes = [i for i in iocs if i["type"] == "sha256"] |
