CI/lint hardening since v1.6.14: clippy on stable with the MSRV single-sourced from Cargo.toml, cargo-deny dependency-audit gate with first-party git sources allowed (#15), and audience-based comment-guard (#17). No library code changes.
2.2 KiB
src/online.rs — SSRF guard
Threat model
The keyserver URL is operator-supplied and the bearer token is confidential.
After validate_keyserver_url checked the host at config time, an attacker
who controls the keyserver's DNS can rebind it to 169.254.169.254 (cloud
metadata) or an RFC1918 host in the window between validation and the actual
POST, exfiltrating the key material and the Authorization token.
Defence
Resolve the host once just before the POST, reject any blocked IP, and pin
the ureq connection to those validated addresses so a subsequent DNS flip
cannot redirect the request. Use redirects(0) so a public URL can't
30x-redirect to an internal host.
Resolver wiring pitfall (PinnedResolver)
ureq 3 replaced v2's resolver closure with the Resolver trait. The agent
MUST be built through Agent::with_parts to take a custom one:
Agent::new_with_config silently keeps the default resolver, which would
send the request to live DNS and quietly reopen the rebinding window this
module exists to close. That failure is invisible — it has no symptom short
of an actual attack — so it is pinned by the test
hardened_agent_connects_to_the_pinned_address_not_dns.
Why that test is structured the way it is
resolve_and_guard VALIDATES addresses; hardened_agent PINS them, so a
hostname cannot re-resolve to an internal address between validation and
connection. The validation half is well covered by other tests — but every
one of those still passes if the pinned resolver is never consulted, because
none of them makes a connection. Nothing else in the file proves the agent
honours the pin, and a mis-wired resolver fails OPEN: the connection quietly
falls back to live DNS and the rebinding window this module exists to close
is reopened, with no visible symptom.
The discriminator: pin the agent to a loopback listener the test owns, then
ask it for a host that CANNOT resolve — .test is reserved by RFC 6761 and
never resolves in the public DNS. Only a consulted resolver can turn that
name into a connection; a fallback to live DNS fails instead. No network is
touched, and query (whose guard blocks loopback by design) is not
involved — this drives hardened_agent directly.