use std::net::IpAddr;
pub fn is_private_ip(ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => {
v4.is_loopback() || v4.is_private() || v4.is_link_local() || v4.is_unspecified() || v4.is_broadcast() || v4.octets()[0] == 100 && (v4.octets()[1] & 0xC0) == 64 }
IpAddr::V6(v6) => {
v6.is_loopback() || v6.is_unspecified() || v6.to_ipv4_mapped().is_some_and(|v4| is_private_ip(&IpAddr::V4(v4)))
}
}
}
pub async fn validate_url_not_private(url: &str) -> Result<(), String> {
let parsed = url::Url::parse(url).map_err(|e| format!("Invalid URL '{url}': {e}"))?;
let host = match parsed.host_str() {
Some(h) => h,
None => return Err(format!("URL '{url}' has no host")),
};
if let Ok(ip) = host.parse::<IpAddr>() {
if is_private_ip(&ip) {
return Err(format!(
"URL '{url}' targets private/internal IP address {ip}"
));
}
return Ok(());
}
let port = parsed.port_or_known_default().unwrap_or(80);
let addr = format!("{host}:{port}");
match tokio::net::lookup_host(&addr).await {
Ok(addrs) => {
for socket_addr in addrs {
if is_private_ip(&socket_addr.ip()) {
return Err(format!(
"URL '{}' resolves to private/internal IP address {}",
url,
socket_addr.ip()
));
}
}
}
Err(_) => {
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_private_ip_loopback() {
assert!(is_private_ip(&"127.0.0.1".parse().expect("test")));
assert!(is_private_ip(&"127.0.0.2".parse().expect("test")));
assert!(is_private_ip(&"::1".parse().expect("test")));
}
#[test]
fn test_is_private_ip_rfc1918() {
assert!(is_private_ip(&"10.0.0.1".parse().expect("test")));
assert!(is_private_ip(&"10.255.255.255".parse().expect("test")));
assert!(is_private_ip(&"172.16.0.1".parse().expect("test")));
assert!(is_private_ip(&"172.31.255.255".parse().expect("test")));
assert!(is_private_ip(&"192.168.0.1".parse().expect("test")));
assert!(is_private_ip(&"192.168.255.255".parse().expect("test")));
}
#[test]
fn test_is_private_ip_link_local() {
assert!(is_private_ip(&"169.254.0.1".parse().expect("test")));
assert!(is_private_ip(&"169.254.169.254".parse().expect("test"))); }
#[test]
fn test_is_private_ip_cgnat() {
assert!(is_private_ip(&"100.64.0.1".parse().expect("test")));
assert!(is_private_ip(&"100.127.255.255".parse().expect("test")));
}
#[test]
fn test_is_private_ip_public() {
assert!(!is_private_ip(&"8.8.8.8".parse().expect("test")));
assert!(!is_private_ip(&"1.1.1.1".parse().expect("test")));
assert!(!is_private_ip(&"203.0.113.1".parse().expect("test")));
}
#[test]
fn test_is_private_ip_v4_mapped_v6() {
assert!(is_private_ip(&"::ffff:127.0.0.1".parse().expect("test")));
assert!(is_private_ip(&"::ffff:10.0.0.1".parse().expect("test")));
assert!(!is_private_ip(&"::ffff:8.8.8.8".parse().expect("test")));
}
#[tokio::test]
async fn test_validate_url_not_private_direct_ip() {
assert!(
validate_url_not_private("http://127.0.0.1/api")
.await
.is_err()
);
assert!(
validate_url_not_private("http://10.0.0.1:8080/api")
.await
.is_err()
);
assert!(
validate_url_not_private("http://192.168.1.1/api")
.await
.is_err()
);
assert!(
validate_url_not_private("http://169.254.169.254/latest/meta-data")
.await
.is_err()
);
}
#[tokio::test]
async fn test_validate_url_not_private_no_host() {
assert!(
validate_url_not_private("data:text/plain,hello")
.await
.is_err()
);
}
}