use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use url::Host;
const BLOCKED_HOSTNAMES: &[&str] = &[
"metadata.google.internal",
"metadata.internal",
"instance-data",
];
fn validate_ipv4(v4: Ipv4Addr) -> Result<(), String> {
let o = v4.octets();
if v4.is_loopback() {
return Err("loopback IP not allowed".into());
}
if v4.is_unspecified() {
return Err("unspecified IP not allowed".into());
}
if v4.is_broadcast() {
return Err("broadcast IP not allowed".into());
}
if v4.is_multicast() {
return Err("multicast IP not allowed".into());
}
if v4.is_link_local() {
return Err("link-local IP not allowed".into());
}
if o[0] == 10 || (o[0] == 172 && (16..=31).contains(&o[1])) || (o[0] == 192 && o[1] == 168) {
return Err("private IP not allowed".into());
}
if o[0] == 100 && (64..=127).contains(&o[1]) {
return Err("CGNAT IP not allowed".into());
}
if o[0] == 0 {
return Err("reserved IP not allowed".into());
}
if o[0] == 192 && o[1] == 0 && o[2] == 0 {
return Err("IETF protocol-assignment IP not allowed".into());
}
if (o[0] == 192 && o[1] == 0 && o[2] == 2)
|| (o[0] == 198 && o[1] == 51 && o[2] == 100)
|| (o[0] == 203 && o[1] == 0 && o[2] == 113)
{
return Err("documentation IP not allowed".into());
}
if o[0] == 198 && (o[1] == 18 || o[1] == 19) {
return Err("benchmarking IP not allowed".into());
}
if o[0] >= 240 {
return Err("reserved IP not allowed".into());
}
Ok(())
}
fn validate_ipv6(v6: Ipv6Addr) -> Result<(), String> {
if let Some(v4) = v6.to_ipv4_mapped() {
return validate_ipv4(v4);
}
if let Some(v4) = v6.to_ipv4() {
return validate_ipv4(v4);
}
if v6.is_loopback() || v6.is_unspecified() || v6.is_multicast() {
return Err("special-use IPv6 not allowed".into());
}
let seg = v6.segments();
if (seg[0] & 0xffc0) == 0xfe80 {
return Err("IPv6 link-local not allowed".into());
}
if (seg[0] & 0xfe00) == 0xfc00 {
return Err("IPv6 ULA not allowed".into());
}
if (seg[0] & 0xffc0) == 0xfec0 {
return Err("IPv6 site-local not allowed".into());
}
if seg[0] == 0x0064 && seg[1] == 0xff9b {
let embedded = Ipv4Addr::new(
(seg[6] >> 8) as u8,
(seg[6] & 0xff) as u8,
(seg[7] >> 8) as u8,
(seg[7] & 0xff) as u8,
);
return validate_ipv4(embedded)
.map_err(|e| format!("NAT64-embedded address rejected: {}", e));
}
if seg[0] == 0x2002 {
let embedded = Ipv4Addr::new(
(seg[1] >> 8) as u8,
(seg[1] & 0xff) as u8,
(seg[2] >> 8) as u8,
(seg[2] & 0xff) as u8,
);
return validate_ipv4(embedded)
.map_err(|e| format!("6to4-embedded address rejected: {}", e));
}
Ok(())
}
pub fn validate_ip(ip: IpAddr) -> Result<(), String> {
match ip {
IpAddr::V4(v4) => validate_ipv4(v4),
IpAddr::V6(v6) => validate_ipv6(v6),
}
}
#[derive(Debug, Clone)]
pub struct ValidatedTarget {
pub host: String,
pub addr: SocketAddr,
}
fn check_hostname(host_lower: &str) -> Result<(), String> {
if host_lower == "localhost" || host_lower.ends_with(".localhost") {
return Err("localhost not allowed".into());
}
if BLOCKED_HOSTNAMES
.iter()
.any(|b| host_lower == *b || host_lower.ends_with(&format!(".{}", b)))
{
return Err(format!("blocked hostname: {}", host_lower));
}
Ok(())
}
pub fn validate_public_url_target(url: &url::Url) -> Result<ValidatedTarget, String> {
let host = url.host().ok_or("URL must have a host")?;
let port = url.port_or_known_default().ok_or("URL must have a port")?;
match host {
Host::Ipv4(v4) => {
validate_ipv4(v4)?;
Ok(ValidatedTarget {
host: v4.to_string(),
addr: SocketAddr::new(IpAddr::V4(v4), port),
})
}
Host::Ipv6(v6) => {
validate_ipv6(v6)?;
Ok(ValidatedTarget {
host: v6.to_string(),
addr: SocketAddr::new(IpAddr::V6(v6), port),
})
}
Host::Domain(domain) => {
let host_lower = domain.to_lowercase();
check_hostname(&host_lower)?;
let addrs: Vec<SocketAddr> =
std::net::ToSocketAddrs::to_socket_addrs(&(host_lower.as_str(), port))
.map_err(|e| format!("DNS resolution failed for {}: {}", host_lower, e))?
.collect();
if addrs.is_empty() {
return Err(format!("no addresses resolved for {}", host_lower));
}
for sa in &addrs {
validate_ip(sa.ip())?;
}
Ok(ValidatedTarget {
host: host_lower,
addr: addrs[0],
})
}
}
}
pub fn validate_public_url_host(url: &url::Url) -> Result<(), String> {
validate_public_url_target(url).map(|_| ())
}
#[cfg(test)]
mod tests {
use super::*;
fn check(u: &str) -> Result<(), String> {
validate_public_url_host(&url::Url::parse(u).expect("parse"))
}
#[test]
fn ipv6_literals_are_rejected() {
for u in [
"http://[::1]:11434/",
"http://[::ffff:127.0.0.1]/",
"http://[::ffff:169.254.169.254]/latest/meta-data/",
"http://[fe80::1]/",
"http://[fc00::1]/",
"http://[fec0::1]/",
"http://[::]/",
"http://[64:ff9b::7f00:1]/",
"http://[2002:7f00:1::]/",
] {
assert!(check(u).is_err(), "should be rejected: {}", u);
}
}
#[test]
fn public_ipv6_is_allowed() {
assert!(check("http://[2606:4700:4700::1111]/").is_ok());
}
#[test]
fn ipv4_special_ranges_are_rejected() {
for u in [
"http://127.0.0.1/",
"http://127.1/",
"http://10.0.0.1/",
"http://172.16.0.1/",
"http://192.168.1.1/",
"http://169.254.169.254/latest/meta-data/",
"http://100.64.0.1/",
"http://0.0.0.0/",
"http://192.0.0.1/",
"http://198.18.0.1/",
"http://240.0.0.1/",
"http://255.255.255.255/",
] {
assert!(check(u).is_err(), "should be rejected: {}", u);
}
}
#[test]
fn public_ipv4_is_allowed() {
assert!(check("http://1.1.1.1/").is_ok());
assert!(check("http://93.184.216.34/").is_ok());
}
#[test]
fn blocked_names_are_rejected() {
for u in [
"http://localhost/",
"http://LOCALHOST/",
"http://app.localhost/",
"http://metadata.google.internal/",
"http://x.metadata.internal/",
] {
assert!(check(u).is_err(), "should be rejected: {}", u);
}
}
#[test]
fn unresolvable_names_are_rejected() {
assert!(check("http://this-name-does-not-exist.invalid/").is_err());
}
#[test]
fn validated_target_host_has_no_brackets() {
let target =
validate_public_url_target(&url::Url::parse("http://[2606:4700:4700::1111]/").unwrap())
.expect("public address");
assert_eq!(target.host, "2606:4700:4700::1111");
assert_eq!(target.addr.port(), 80);
}
}