use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum Scheme {
Http,
Https,
}
pub(crate) struct ValidatedTarget {
pub(crate) scheme: Scheme,
pub(crate) host: String,
pub(crate) port: u16,
pub(crate) addrs: Vec<IpAddr>,
pub(crate) path_and_query: String,
}
pub(crate) fn validate_url(url: &str) -> Result<ValidatedTarget, String> {
let parsed = url::Url::parse(url).map_err(|e| format!("Invalid URL: {e}"))?;
let scheme = match parsed.scheme() {
"http" => Scheme::Http,
"https" => Scheme::Https,
s => return Err(format!("Blocked scheme '{s}': only http/https allowed")),
};
let host = parsed
.host_str()
.ok_or_else(|| "URL has no host".to_string())?
.to_string();
let host_lower = host.to_lowercase();
if host_lower == "localhost"
|| host_lower == "localhost.localdomain"
|| host_lower.ends_with(".localhost")
{
return Err("Blocked: localhost access not allowed".to_string());
}
let port = parsed.port().unwrap_or(match scheme {
Scheme::Https => 443,
Scheme::Http => 80,
});
let addrs = crate::dns::resolve_to_ips(&host);
for ip in &addrs {
if is_dangerous_ip(*ip) {
return Err(format!(
"Blocked: {host} resolves to private/internal IP {ip}"
));
}
}
let path_and_query = match parsed.query() {
Some(q) => format!("{}?{q}", parsed.path()),
None => parsed.path().to_string(),
};
Ok(ValidatedTarget {
scheme,
host,
port,
addrs,
path_and_query,
})
}
#[allow(dead_code)] pub(crate) fn validate_url_for_ssrf(url: &str) -> Result<(), String> {
validate_url(url).map(|_| ())
}
pub(crate) fn is_dangerous_ipv4(ip: Ipv4Addr) -> bool {
if ip.is_loopback() {
return true;
}
let o = ip.octets();
if o[0] == 10 {
return true;
}
if o[0] == 172 && (16..=31).contains(&o[1]) {
return true;
}
if o[0] == 192 && o[1] == 168 {
return true;
}
if o[0] == 169 && o[1] == 254 {
return true;
}
ip.is_broadcast()
}
pub(crate) fn is_dangerous_ipv6(ip: Ipv6Addr) -> bool {
if ip.is_loopback() {
return true;
}
let s = ip.segments();
if (s[0] & 0xffc0) == 0xfe80 {
return true;
}
if (s[0] & 0xfe00) == 0xfc00 {
return true;
}
if let Some(v4) = ip.to_ipv4_mapped() {
return is_dangerous_ipv4(v4);
}
false
}
pub(crate) fn is_dangerous_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => is_dangerous_ipv4(v4),
IpAddr::V6(v6) => is_dangerous_ipv6(v6),
}
}