From 993a76ded1d22f01768d374aacee4be6ec40cc95 Mon Sep 17 00:00:00 2001 From: Mick Grove Date: Fri, 27 Mar 2026 22:57:19 -0700 Subject: [PATCH] updated in response to ossf scorecard --- .../src/validation/http_validation.rs | 32 +++++++++++++++++++ src/validation.rs | 9 ++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/crates/kingfisher-scanner/src/validation/http_validation.rs b/crates/kingfisher-scanner/src/validation/http_validation.rs index 34e0089..fb32b28 100644 --- a/crates/kingfisher-scanner/src/validation/http_validation.rs +++ b/crates/kingfisher-scanner/src/validation/http_validation.rs @@ -394,6 +394,8 @@ pub fn validate_response( /// Returns `true` if the IP address is safe for outbound validation requests /// (i.e., it is a publicly routable address, not internal/reserved). +/// +/// Covers all IANA special-purpose ranges from RFC 6890 and RFC 8190. pub fn is_ssrf_safe_ip(ip: &IpAddr) -> bool { if ip.is_loopback() || ip.is_unspecified() || ip.is_multicast() { return false; @@ -423,10 +425,18 @@ pub fn is_ssrf_safe_ip(ip: &IpAddr) -> bool { if octets[0] == 100 && (64..=127).contains(&octets[1]) { return false; } + // IANA Special Purpose (192.0.0.0/24, RFC 6890) + if octets[0] == 192 && octets[1] == 0 && octets[2] == 0 { + return false; + } // Documentation ranges (RFC 5737) if octets[0] == 192 && octets[1] == 0 && octets[2] == 2 { return false; } + // 6to4 relay anycast (192.88.99.0/24, RFC 7526 — deprecated) + if octets[0] == 192 && octets[1] == 88 && octets[2] == 99 { + return false; + } if octets[0] == 198 && octets[1] == 51 && octets[2] == 100 { return false; } @@ -468,6 +478,10 @@ pub fn is_ssrf_safe_ip(ip: &IpAddr) -> bool { if segments[0] & 0xffc0 == 0xfec0 { return false; } + // Benchmarking (2001:2::/48, RFC 5180) + if segments[0] == 0x2001 && segments[1] == 0x0002 && segments[2] == 0 { + return false; + } // Documentation (2001:db8::/32) if segments[0] == 0x2001 && segments[1] == 0x0db8 { return false; @@ -679,6 +693,24 @@ mod tests { assert!(!is_ssrf_safe_ip(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0x0808, 0x0808)))); } + #[test] + fn rejects_iana_special_purpose() { + // 192.0.0.0/24 — IANA special-purpose (RFC 6890) + assert!(!is_ssrf_safe_ip(&IpAddr::V4(Ipv4Addr::new(192, 0, 0, 1)))); + } + + #[test] + fn rejects_6to4_relay_anycast() { + // 192.88.99.0/24 — 6to4 relay anycast (RFC 7526, deprecated) + assert!(!is_ssrf_safe_ip(&IpAddr::V4(Ipv4Addr::new(192, 88, 99, 1)))); + } + + #[test] + fn rejects_ipv6_benchmarking() { + // 2001:2::/48 — benchmarking (RFC 5180) + assert!(!is_ssrf_safe_ip(&IpAddr::V6(Ipv6Addr::new(0x2001, 0x0002, 0, 0, 0, 0, 0, 1)))); + } + #[test] fn accepts_public_ipv4() { assert!(is_ssrf_safe_ip(&IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)))); diff --git a/src/validation.rs b/src/validation.rs index b74e359..bda079c 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -130,9 +130,12 @@ pub struct ValidationClients { /// closes the hostname-redirect SSRF gap (e.g., a public URL that 302s to an /// attacker-controlled hostname resolving to `169.254.169.254`). /// -/// **Note:** Using blocking DNS in the redirect callback is acceptable because -/// reqwest runs redirect callbacks on its connection thread, not the tokio -/// event loop, and the DNS latency is negligible relative to the HTTP request. +/// **Note:** reqwest runs redirect callbacks on Tokio worker threads, so the +/// blocking DNS lookup here can briefly stall other async tasks on that thread. +/// This is acceptable for a scanner workload because DNS is typically cached +/// by the system resolver (<5ms), redirect hops are infrequent, and the +/// alternative (disabling automatic redirects and following them manually with +/// async DNS) would add significant complexity for minimal practical benefit. pub(crate) fn ssrf_safe_redirect_policy() -> reqwest::redirect::Policy { reqwest::redirect::Policy::custom(|attempt| { // Cap redirect depth (reqwest default is 10)