aboutsummaryrefslogtreecommitdiffstats
path: root/tests
AgeCommit message (Collapse)AuthorFilesLines
24 hoursfix: take the sender domain from the address, not the display nameDanilo M.2-1/+8
_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
25 hoursfix: prefer the observed address over an attacker's HELO literalDanilo M.1-0/+22
_extract_ip returned the FIRST bracketed IP in a Received header value. Postfix (and others) write the client's own HELO/EHLO argument first and the address it actually observed on the connection second: Received: from [198.51.100.7] (unknown [203.0.113.99]) by mx... The first bracket is entirely attacker-chosen; a client can HELO with any literal it likes. sending_ip() returned 198.51.100.7, reporting whoever the attacker named rather than 203.0.113.99, the address the accepting server itself wrote. This needs no forged extra hop, only a client that HELOs with an address literal, and the module's own docstring already stated the intended answer ("the bracketed literal after the connecting hostname") without the code implementing it. _extract_ip now collects every bracketed, ipaddress-valid literal with its position and, when there is more than one, prefers the last one appearing before " by " (the accepting server's own clause, and the one closest to it). A header with a single bracketed IP or no " by " token keeps the previous single-candidate behaviour, so simple.eml (203.0.113.42) and forged-chain.eml (203.0.113.99, item 3's own mutation-checked test) are unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
25 hoursfix: re-bracket an IPv6 host when stripping URL userinfoDanilo M.1-0/+12
parts.hostname returns an IPv6 literal WITHOUT its brackets (2001:db8::1, not [2001:db8::1]), and _netloc_without_userinfo reassembled f"{host}:{port}" directly from it: redact.url("http://victim@[2001:db8::1]:8080/p?x=1") -> "http://2001:db8::1:8080/p?x=REDACTED" That string is not parseable back into a host and a port, and the digits after the second-to-last colon are not even part of the address any more. Reporting it means the abuse desk cannot identify the host at all, or misreads it, which is the same class of harm as reporting a wrong IP outright. _netloc_without_userinfo re-adds brackets whenever the hostname contains ":", so an IPv6 host now survives userinfo stripping exactly as an IPv4 or named host already did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
25 hoursfix: redact a valueless query token whole, not as a kept nameDanilo M.1-0/+14
parse_qsl reads "?victim@example.org" as the pair ("victim@example.org", ""), and the code kept parameter NAMES because they fingerprint the phishing kit. A name that is itself an address is not a name, so keeping it published the recipient's identifier verbatim (percent-decoded, no less: %40 fools no consumer). _redact_kv_string now splits the query/fragment string by hand on "&" and ";" and inspects each token's own name: one containing "@" is a value that landed in name position and is redacted WHOLE ("?REDACTED" rather than "?victim%40example.org=REDACTED"); an ordinary name still keeps its shape ("?flag" stays "?flag=REDACTED", "?t=1&t=2" stays "?t=REDACTED&t=REDACTED"). This also exposed a second leak reachable through the same fixture: parse._suspect_segments() ran redact.suspect_path_segments()'s decode-and-check predicate (meant for a querystring smuggled past percent-encoding into a PATH segment) against a raw query VALUE, so a plaintext "?e=you@example.org" reproduced the address in the manifest's suspect_path_segments flag even though the URL itself was correctly redacted. redact.suspect_path_segments() now exposes the opaque-shape half of its check as _looks_opaque(), and parse.py uses only that half against query values: a query value is always fully redacted regardless, so the flag may hint at its shape but must never reproduce it. Turns tests.test_parse.TestIocAssembly.test_the_address_does_not_survive_any_url_shape green, and closes the gap test_no_ioc_holds_a_recipient_address had been passing over with URL redaction fully disabled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
25 hourstest: wire leaky.eml into the anti-leak suite (red, defect 2)Danilo M.2-1/+29
test_no_ioc_holds_a_recipient_address grepped for the literal "example.org", and every existing fixture hides the recipient address as base64, so URL redaction could be disabled entirely and both this test and test_cli's counterpart stayed green. leaky.eml carries you@example.org in five URL shapes plus a From display-name trap. Adding it to the fixture list, plus a new test asserting on the raw address and its percent-encoded form, turns the suite red: the valueless-query-parameter defect (redact.py) currently lets ?victim@example.org through as a kept parameter NAME. Left failing on purpose; the next commit fixes redact.py and turns it green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
26 hoursfeat: command line for init and parseDanilo M.1-0/+167
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
26 hoursfeat: first-run config builderDanilo M.1-0/+185
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
26 hoursfix: reject non-list relays and empty cases valuesDanilo M.1-0/+29
An empty cases string resolved to Path("") = cwd, scattering evidence wherever the command happened to run. A string trusted_relays (easy to hand-write without brackets) iterated as characters, failing on '1' with an error naming nothing findable in the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
26 hoursfeat: read and validate the configDanilo M.1-0/+88
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
27 hoursfeat: assemble IOCs in the manifest's shapeDanilo M.1-0/+63
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
27 hoursfeat: case directory with an atomically written manifestDanilo M.1-0/+102
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
27 hoursfeat: extract URLs and attachment hashes, fetching nothingDanilo M.1-0/+61
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
27 hoursfeat: extract sender domains and auth verdictsDanilo M.1-0/+32
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
27 hoursfeat: walk the Received chain to the trust boundaryDanilo M.1-0/+68
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
27 hoursfix: redact fragment and strip userinfo, flag hidden query in pathDanilo M.1-0/+42
The query string was not the only place a recipient identifier can hide. A fragment (#e=victim@...) is published as-is since we report the URL's literal text, not what a browser would send. Userinfo (user:pass@host) leaks a credential as well as an identifier, so it is stripped outright rather than redacted in place. A path segment can also smuggle an encoded query (%3Fe=victim@...); suspect_path_segments now flags a segment that decodes to something containing '=' or '@', still leaving the decision to redact or not to human review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
27 hoursfeat: redact recipient identifiers inside URLsDanilo M.1-0/+106
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
27 hourstest: fixtures for the parser, documentation ranges onlyDanilo M.4-0/+81
Four hand-written messages using example.org, example.invalid and the RFC 5737 documentation IP ranges. No real phishing sample goes in this repository: it would carry the recipient identifiers this tool exists to keep out of reports, and a repository is potentially public. forged-chain.eml is the one that matters. The attacker prepends two Received headers naming an innocent third party, so a parser that walks past the trust boundary reports 198.51.100.7 rather than 203.0.113.99. Weekdays verified with date(1) rather than written from memory, since an RFC2822 parser validates the day against the date and a wrong one is indistinguishable from a malformed header. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
27 hoursfeat: package skeletonDanilo M.2-0/+28
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R