Files
freemkv-keysources/docs/online-ssrf-guard.md
T
Matthew Jackson 599422ef22 release: 1.7.0
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.
2026-09-02 12:27:48 -07:00

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.