Labels: security, enhancement, priority:medium
Severity: MEDIUM (latent while UnifiedPush is disabled)
Area: push/unifiedpush
Files:
- src/push/endpoint_guard.rs (
validate_endpoint)
- src/push/unifiedpush.rs (
send_to_token)
Problem
endpoint_guard::validate_endpoint resolves a domain host and refuses the
endpoint if any resolved address is non-public. That closes the straightforward
SSRF path from #4, but it leaves a time-of-check / time-of-use gap:
guard: lookup_host("evil.example") -> 93.184.216.34 (public, accepted)
reqwest: connects, resolves again -> 169.254.169.254 (internal)
An attacker who controls the authoritative DNS for a name can serve a public
address to the validation lookup and an internal one to the connection, with a
TTL short enough that no cache absorbs the difference. Winning the race is
harder than the IP-literal bypass #4 closed, but it is a well-known technique
and the guard should not be described as complete until it is closed.
The limitation is documented in the module docs and in docs/unifiedpush.md
rather than left implicit.
Proposal
Resolve once and connect to the address that was validated, so no second
resolution can occur:
The dedicated client this originally called for already exists:
UnifiedPushService::build_client() was added alongside #4 to refuse redirects,
which were a separate first-hop bypass of the same guard. The remaining work is
just to give that client a pinned resolver.
- Implement
reqwest::dns::Resolve backed by the addresses validate_endpoint
already checked, and wire it into build_client via
ClientBuilder::dns_resolver.
- Keep FCM on the shared client (src/main.rs:69-76). It talks to a fixed Google
endpoint and gains nothing from a pinned resolver.
- Preserve the existing timeouts (2 s connect, 5 s total) and the no-redirect
policy already set on that client.
Alternative considered and rejected: rewriting the URL to the validated IP and
setting a Host header. That breaks TLS SNI and certificate validation, which
trades one security property for another.
Acceptance criteria
Labels: security, enhancement, priority:medium
Severity: MEDIUM (latent while UnifiedPush is disabled)
Area: push/unifiedpush
Files:
validate_endpoint)send_to_token)Problem
endpoint_guard::validate_endpointresolves a domain host and refuses theendpoint if any resolved address is non-public. That closes the straightforward
SSRF path from #4, but it leaves a time-of-check / time-of-use gap:
An attacker who controls the authoritative DNS for a name can serve a public
address to the validation lookup and an internal one to the connection, with a
TTL short enough that no cache absorbs the difference. Winning the race is
harder than the IP-literal bypass #4 closed, but it is a well-known technique
and the guard should not be described as complete until it is closed.
The limitation is documented in the module docs and in
docs/unifiedpush.mdrather than left implicit.
Proposal
Resolve once and connect to the address that was validated, so no second
resolution can occur:
The dedicated client this originally called for already exists:
UnifiedPushService::build_client()was added alongside #4 to refuse redirects,which were a separate first-hop bypass of the same guard. The remaining work is
just to give that client a pinned resolver.
reqwest::dns::Resolvebacked by the addressesvalidate_endpointalready checked, and wire it into
build_clientviaClientBuilder::dns_resolver.endpoint and gains nothing from a pinned resolver.
policy already set on that client.
Alternative considered and rejected: rewriting the URL to the validated IP and
setting a
Hostheader. That breaks TLS SNI and certificate validation, whichtrades one security property for another.
Acceptance criteria
src/push/endpoint_guard.rsanddocs/unifiedpush.mdare removed or rewritten to match the new behaviour.