| Age | Commit message (Collapse) | Author | Files | Lines |
|
Four holes of one class, all in the path that admits a query.
ipaddress.ip_address accepts a scope id since Python 3.9, so
"fe80::1%victim@example.org" passed _is_ip, is_queryable returned on
that branch without inspecting the string further, and rdap.query_ip
interpolated the whole thing into the query URL unquoted. A recipient
identifier reached a registry, which is exactly the disclosure the
fourth property exists to prevent and exactly what the domain-branch
fix closed. Reject a scope id at the admission point. fe80::/10 is
link-local and never a legitimate RDAP target anyway.
The ip branch of worklist() dropped an unparseable value silently
rather than funnelling it through add() like the other two branches,
so a mangled indicator vanished from the manifest instead of showing
up as unusable. It now goes through add(), and is_queryable grew the
check that makes that classification correct: an all-digit last label
is never a TLD, so "999.999.999.999" is a malformed IP rather than a
host to ask a registry about.
Two behaviours had no test and both mutations survived. _host_of's
root-dot strip is now covered through the url branch, since failing to
fold discloses one host to a registry twice; the .lower() half was
dead work because urlsplit already lowercases, and is dropped with a
comment saying so. The two-label requirement is asserted on
is_queryable directly, because a single-label name matches no tld in
the bootstrap and a resolve test could never fail whatever the
validator decides.
The refusal message named no reason, leaving a reviewer unable to tell
whether the parser mangled a legitimate host or the attacker planted
something. Two cases now: a non-ASCII name is plausibly a real IDN
indicator to chase by hand, an illegal character is hostile.
Also let KeyboardInterrupt and SystemExit through resolve()'s handler,
so stopping a long run is not recorded as a failed query on whichever
indicator was in flight.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrfqr2xqQfhtXCscU7zrdz
|
|
THE FOURTH PROPERTY was breached through the domain branch of worklist().
The url branch is cleaned by _host_of, which uses urlsplit().hostname,
and the domain branch did only value.strip(".").lower(). rdap.query_domain
then interpolates that value into the fetch URL with no quoting.
parse._domain_of takes everything after the @ of a From, Sender or
Reply-To addr-spec, and email.utils.parseaddr permits /, ?, # and %
there, so the whole shape is attacker-controlled through a header they
own. A From of `Bank <phish@victim%40example.org.invalid>` sent the
recipient's own address to a registry, which is precisely the identity
disclosure the property exists to prevent, reaching a third party.
`a/../../x.invalid` escaped the /domain/ endpoint altogether, and query,
fragment and space values all reached the wire.
Fixed at the SINGLE admission point rather than in the offending branch,
because per-branch validation is what failed here: one branch was
cleaned, the next was written without it. is_queryable() now guards
worklist()'s add(), so domain, url and any future branch pass through it.
A rejected value is not dropped. It keeps an entry with an empty abuse
list and an error saying it was never queried, following the rule
suspect_path_segments already sets: flagged and visible to the user
during review, because silent was the bug.
A non-ASCII host is refused rather than encoded to punycode. Guessing the
encoding of an attacker-supplied name is a query that cannot be
justified, and the ceiling is noted in a ponytail comment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrfqr2xqQfhtXCscU7zrdz
|
|
Failure is per query and never stops the run: a timeout on one indicator
must not cost the contacts that did resolve, and a missing contact is a
normal outcome rather than an error.
Bootstraps are passed in rather than fetched here, so this stays
testable with no network and the caller owns the cache policy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrfqr2xqQfhtXCscU7zrdz
|
|
Adds the fourth non-negotiable property: a query carries a bare host or
IP and never a URL. An RDAP query discloses what the user is looking at,
and a URL path can carry recipient identity that parse deliberately
flags rather than redacts, because a path segment may be the thing being
reported. That decision is safe only while the URL stays local.
Property 1 governs what is published and a query appears in no report,
so property 1 does not cover this and this property does.
Hosts fold, so twenty URLs on one host make one query while the item
keeps every indicator id behind it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrfqr2xqQfhtXCscU7zrdz
|