diff --git a/crates/kingfisher-scanner/src/validation/http_validation.rs b/crates/kingfisher-scanner/src/validation/http_validation.rs index 5638907..34e0089 100644 --- a/crates/kingfisher-scanner/src/validation/http_validation.rs +++ b/crates/kingfisher-scanner/src/validation/http_validation.rs @@ -528,6 +528,13 @@ pub async fn check_url_resolvable( Ok(()) } +/// Backwards-compatible wrapper: checks URL resolvability with SSRF protection +/// enabled (i.e., `allow_internal_ips = false`). +#[deprecated(since = "0.1.0", note = "use check_url_resolvable(url, allow_internal_ips) instead")] +pub async fn check_url_resolvable_safe(url: &Url) -> Result<(), Box> { + check_url_resolvable(url, false).await +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/kingfisher-scanner/src/validation/mod.rs b/crates/kingfisher-scanner/src/validation/mod.rs index fd5dcc2..e1914a7 100644 --- a/crates/kingfisher-scanner/src/validation/mod.rs +++ b/crates/kingfisher-scanner/src/validation/mod.rs @@ -59,9 +59,11 @@ pub use utils::{find_closest_variable, process_captures}; pub use validation_body::{as_str, clone_as_string, from_string, ValidationResponseBody}; #[cfg(feature = "validation-http")] +#[allow(deprecated)] pub use http_validation::{ - build_request_builder, check_url_resolvable, generate_http_cache_key_parts, is_ssrf_safe_ip, - parse_http_method, process_headers, retry_multipart_request, retry_request, validate_response, + build_request_builder, check_url_resolvable, check_url_resolvable_safe, + generate_http_cache_key_parts, is_ssrf_safe_ip, parse_http_method, process_headers, + retry_multipart_request, retry_request, validate_response, }; #[cfg(feature = "validation-aws")] diff --git a/src/validation.rs b/src/validation.rs index 5ad795f..b74e359 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -153,20 +153,27 @@ pub(crate) fn ssrf_safe_redirect_policy() -> reqwest::redirect::Policy { } else { // Hostname: resolve synchronously and check all resolved IPs. let port = url.port().unwrap_or(if url.scheme() == "https" { 443 } else { 80 }); - if let Ok(addrs) = std::net::ToSocketAddrs::to_socket_addrs(&(host, port as u16)) { - for addr in addrs { - if !kingfisher_scanner::validation::is_ssrf_safe_ip(&addr.ip()) { - return attempt.error(format!( - "SSRF protection: redirect to '{}' resolves to non-public IP {} — blocked", - host, - addr.ip() - )); + match std::net::ToSocketAddrs::to_socket_addrs(&(host, port as u16)) { + Ok(addrs) => { + for addr in addrs { + if !kingfisher_scanner::validation::is_ssrf_safe_ip(&addr.ip()) { + return attempt.error(format!( + "SSRF protection: redirect to '{}' resolves to non-public IP {} — blocked", + host, + addr.ip() + )); + } } } + Err(e) => { + // Fail closed: if we cannot resolve the hostname, we + // cannot guarantee the redirect target is SSRF-safe. + return attempt.error(format!( + "SSRF protection: cannot resolve redirect host '{}' ({}) — blocked", + host, e + )); + } } - // If DNS resolution fails, allow the redirect — reqwest will - // fail on connect anyway, and we don't want to silently block - // valid hostnames that are transiently unresolvable. } } attempt.follow()