use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, ToSocketAddrs};
use ipnetwork::{Ipv4Network, Ipv6Network};
use url::Url;
use crate::error::{OpenApiError, Result};
static BLOCKED_IPV4_RANGES: &[&str] = &[
"0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.2.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4", "255.255.255.255/32", ];
static BLOCKED_IPV6_RANGES: &[&str] = &[
"::1/128", "::ffff:0:0/96", "64:ff9b::/96", "100::/64", "fe80::/10", "fc00::/7", "ff00::/8", ];
pub fn validate_url_for_ssrf(url: &Url) -> Result<()> {
let host = url
.host_str()
.ok_or_else(|| OpenApiError::SsrfBlocked("URL has no host".to_string()))?;
let host_lower = host.to_lowercase();
if host_lower == "localhost"
|| host_lower == "localhost.localdomain"
|| host_lower.ends_with(".localhost")
|| host_lower.ends_with(".local")
{
return Err(OpenApiError::SsrfBlocked(format!(
"localhost hostname blocked: {}",
host
)));
}
if let Ok(ip) = host.parse::<IpAddr>() {
return validate_ip_for_ssrf(ip);
}
let socket_addrs = format!("{}:80", host);
if let Ok(addrs) = socket_addrs.to_socket_addrs() {
for addr in addrs {
validate_ip_for_ssrf(addr.ip())?;
}
}
Ok(())
}
fn validate_ip_for_ssrf(ip: IpAddr) -> Result<()> {
match ip {
IpAddr::V4(ipv4) => validate_ipv4_for_ssrf(ipv4),
IpAddr::V6(ipv6) => validate_ipv6_for_ssrf(ipv6),
}
}
fn validate_ipv4_for_ssrf(ip: Ipv4Addr) -> Result<()> {
for range_str in BLOCKED_IPV4_RANGES {
if let Ok(network) = range_str.parse::<Ipv4Network>()
&& network.contains(ip)
{
return Err(OpenApiError::SsrfBlocked(format!(
"IP address {} is in blocked range {}",
ip, range_str
)));
}
}
Ok(())
}
fn validate_ipv6_for_ssrf(ip: Ipv6Addr) -> Result<()> {
if let Some(ipv4) = ip.to_ipv4_mapped() {
return validate_ipv4_for_ssrf(ipv4);
}
for range_str in BLOCKED_IPV6_RANGES {
if let Ok(network) = range_str.parse::<Ipv6Network>()
&& network.contains(ip)
{
return Err(OpenApiError::SsrfBlocked(format!(
"IP address {} is in blocked range {}",
ip, range_str
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_allows_external_urls() {
let url: Url = "https://api.github.com/users".parse().unwrap();
let _ = validate_url_for_ssrf(&url);
}
#[test]
fn test_blocks_localhost() {
let urls = [
"http://localhost/",
"http://localhost:8080/",
"http://LOCALHOST/",
"http://localhost.localdomain/",
"http://test.localhost/",
];
for url_str in urls {
let url: Url = url_str.parse().unwrap();
assert!(
validate_url_for_ssrf(&url).is_err(),
"Should block: {}",
url_str
);
}
}
#[test]
fn test_blocks_private_ipv4() {
let urls = [
"http://127.0.0.1/",
"http://10.0.0.1/",
"http://172.16.0.1/",
"http://192.168.1.1/",
"http://169.254.169.254/", ];
for url_str in urls {
let url: Url = url_str.parse().unwrap();
assert!(
validate_url_for_ssrf(&url).is_err(),
"Should block: {}",
url_str
);
}
}
#[test]
fn test_blocks_loopback_ipv6() {
let url: Url = "http://[::1]/".parse().unwrap();
assert!(validate_url_for_ssrf(&url).is_err());
}
#[test]
fn test_allows_public_ips() {
let ips = [
"8.8.8.8", "1.1.1.1", "208.67.222.222", ];
for ip in ips {
let url: Url = format!("http://{}/", ip).parse().unwrap();
assert!(
validate_url_for_ssrf(&url).is_ok(),
"Should allow public IP: {}",
ip
);
}
}
}