fix: reject private IPs after DNS resolution (SSRF/DNS-rebinding)

The SSRF guard only checked the host string, so an attacker-owned domain
resolving to 169.254.169.254 / 10.x (DNS rebinding, TOCTOU) bypassed it.
Add a reqwest dns::Resolve (SafeResolver) that drops resolved addresses in
private ranges, wired into all four crawler/analysis clients — it fires per
connection so it also covers redirect hops. Also close the is_private_ip
IPv6-embedding gaps (IPv4-compatible ::/96, NAT64 64:ff9b::/96, 6to4
2002::/16 all now unwrap to the embedded IPv4).

Bump to 0.124.10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-07 21:52:30 +02:00
parent 61669aac3f
commit ff4ca964f5
6 changed files with 155 additions and 10 deletions

View File

@@ -476,6 +476,10 @@ async fn spawn_analysis_daemon(
// endpoint is a single admin-configured URL), so the policy
// enforces scheme + private-IP only.
.redirect(crate::crawler::safety::public_redirect_policy())
// Filter resolved IPs: a hostname that resolves to an internal
// address (DNS rebinding) is refused at connect time, not just
// by the string check above.
.dns_resolver(crate::crawler::safety::safe_dns_resolver())
.build()
.context("build analysis http client")?;
let vision = crate::analysis::vision::VisionClient::new(http, cfg);
@@ -502,6 +506,7 @@ async fn spawn_analysis_daemon(
// uptime + vision health.
.no_proxy()
.redirect(crate::crawler::safety::public_redirect_policy())
.dns_resolver(crate::crawler::safety::safe_dns_resolver())
.build()
.context("build vision readiness http client")?;
Some(Arc::new(crate::analysis::daemon::HttpVisionReadiness {
@@ -586,6 +591,10 @@ async fn spawn_crawler_daemon(
.redirect(crate::crawler::safety::safe_redirect_policy(
cfg.download_allowlist.clone(),
))
// Reject any host that resolves to a private/internal IP (DNS
// rebinding), complementing the string-level allowlist/private-IP
// check which can't see post-resolution addresses.
.dns_resolver(crate::crawler::safety::safe_dns_resolver())
.cookie_provider(Arc::clone(&cookie_jar));
if let Some(ua) = &cfg.user_agent {
http_builder = http_builder.user_agent(ua);