diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-09 09:19:27 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-09 09:19:27 +0200 |
| commit | 46e042a33bdd529da71115649d1736e703b676c8 (patch) | |
| tree | d0252dad8718c5091507360a20cca8a3b8518a6d /tests | |
| parent | a3ff8f3b056c708872c5aedb4750d5cd32c5833d (diff) | |
| download | abusectl-46e042a33bdd529da71115649d1736e703b676c8.tar.gz abusectl-46e042a33bdd529da71115649d1736e703b676c8.zip | |
feat: read abuse addresses from a jCard, strictly
Only an entity whose roles contain abuse counts. No fallback to a
technical or registrant contact, who is a named human that never
volunteered for abuse mail, and no fallback to abuse@<domain> by
convention: for a phishing domain that mailbox belongs to the attacker,
so constructing it would confirm both the catch and that the reporter's
address is live.
Addresses are validated where they enter rather than where they are
used, because the value becomes a mail recipient later and a control
character in it is header injection into mail this tool sends.
Entity recursion is depth capped so remote JSON cannot hang the tool.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrfqr2xqQfhtXCscU7zrdz
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_rdap.py | 85 |
1 files changed, 85 insertions, 0 deletions
diff --git a/tests/test_rdap.py b/tests/test_rdap.py index 14a1fd4..b7baa21 100644 --- a/tests/test_rdap.py +++ b/tests/test_rdap.py @@ -176,5 +176,90 @@ class ServerSelection(unittest.TestCase): self.assertIsNone(rdap.server_for_tld("example", self.DNS)) +def _entity(roles, emails, entities=None): + """Build an RDAP entity in real jCard shape.""" + properties = [["version", {}, "text", "4.0"]] + for address in emails: + properties.append(["email", {}, "text", address]) + entity = {"roles": roles, "vcardArray": ["vcard", properties]} + if entities: + entity["entities"] = entities + return entity + + +class AbuseExtraction(unittest.TestCase): + def test_an_abuse_entity_yields_its_address(self): + response = {"entities": [_entity(["abuse"], ["abuse@example.invalid"])]} + self.assertEqual( + rdap.abuse_addresses(response), ["abuse@example.invalid"] + ) + + def test_a_nested_abuse_entity_is_found(self): + """The abuse entity is usually a child of the organisation entity.""" + response = { + "entities": [ + _entity( + ["registrant"], [], + entities=[_entity(["abuse"], ["abuse@example.invalid"])], + ) + ] + } + self.assertEqual( + rdap.abuse_addresses(response), ["abuse@example.invalid"] + ) + + def test_a_technical_only_response_yields_nothing(self): + """A technical contact is a named human who never volunteered to + receive abuse mail. Mailing them is useless and is a small privacy + harm to an uninvolved third party.""" + response = {"entities": [_entity(["technical"], ["someone@example.invalid"])]} + self.assertEqual(rdap.abuse_addresses(response), []) + + def test_every_abuse_address_is_kept(self): + """Some netblocks publish two desks, and picking one arbitrarily + can drop the one that would have answered.""" + response = { + "entities": [ + _entity(["abuse"], ["one@example.invalid", "two@example.invalid"]) + ] + } + self.assertEqual( + rdap.abuse_addresses(response), + ["one@example.invalid", "two@example.invalid"], + ) + + def test_a_newline_in_an_address_is_rejected(self): + """The address becomes a mail recipient in report and submit, so a + CRLF here is header injection into mail this tool sends.""" + response = { + "entities": [ + _entity(["abuse"], ["abuse@example.invalid\r\nBcc: victim@example.org"]) + ] + } + self.assertEqual(rdap.abuse_addresses(response), []) + + def test_a_non_address_is_rejected(self): + response = {"entities": [_entity(["abuse"], ["not an address"])]} + self.assertEqual(rdap.abuse_addresses(response), []) + + def test_recursion_is_depth_capped(self): + """Remote JSON must not be able to hang the tool.""" + deep = _entity(["abuse"], ["deep@example.invalid"]) + for _ in range(10): + deep = _entity(["registrant"], [], entities=[deep]) + self.assertEqual(rdap.abuse_addresses({"entities": [deep]}), []) + + def test_duplicate_addresses_collapse(self): + response = { + "entities": [ + _entity(["abuse"], ["abuse@example.invalid"]), + _entity(["abuse"], ["abuse@example.invalid"]), + ] + } + self.assertEqual( + rdap.abuse_addresses(response), ["abuse@example.invalid"] + ) + + if __name__ == "__main__": unittest.main() |
