updated in response to ossf scorecard

This commit is contained in:
Mick Grove 2026-03-27 22:57:19 -07:00
commit 993a76ded1
2 changed files with 38 additions and 3 deletions

View file

@ -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))));

View file

@ -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)