pub(crate) fn is_private_host(host: &str) -> bool {
const BLOCKED_HOSTS: &[&str] = &[
"localhost",
"127.0.0.1",
"[::1]",
"0.0.0.0",
"169.254.169.254",
"metadata.google.internal",
];
if BLOCKED_HOSTS.iter().any(|&b| host.eq_ignore_ascii_case(b)) {
return true;
}
if let Ok(ip) = host.parse::<std::net::Ipv4Addr>() {
return is_private_ipv4(ip);
}
if let Ok(ip) = host
.trim_matches(|c| c == '[' || c == ']')
.parse::<std::net::Ipv6Addr>()
{
return is_private_ipv6(&ip);
}
false
}
fn is_private_ipv4(ip: std::net::Ipv4Addr) -> bool {
const BLOCKED: &[(u32, u32)] = &[
(0x0000_0000, 0xff00_0000), (0x0a00_0000, 0xff00_0000), (0x6440_0000, 0xffc0_0000), (0x7f00_0000, 0xff00_0000), (0xa9fe_0000, 0xffff_0000), (0xac10_0000, 0xfff0_0000), (0xc000_0000, 0xffff_ff00), (0xc000_0200, 0xffff_ff00), (0xc058_6300, 0xffff_ff00), (0xc0a8_0000, 0xffff_0000), (0xc612_0000, 0xfffe_0000), (0xc633_6400, 0xffff_ff00), (0xcb00_7100, 0xffff_ff00), (0xe000_0000, 0xf000_0000), (0xf000_0000, 0xf000_0000), (0xffff_ffff, 0xffff_ffff), ];
let bits = u32::from(ip);
BLOCKED.iter().any(|&(net, mask)| bits & mask == net)
}
fn is_private_ipv6(ip: &std::net::Ipv6Addr) -> bool {
if let Some(v4) = ip.to_ipv4_mapped().or_else(|| ip.to_ipv4()) {
return is_private_ipv4(v4);
}
let seg = ip.segments();
let s0 = seg[0];
ip.is_loopback()
|| ip.is_unspecified()
|| (s0 == 0x0100 && seg[1] == 0 && seg[2] == 0 && seg[3] == 0) || (s0 == 0x2001 && seg[1] == 0) || (s0 == 0x2001 && seg[1] & 0xfff0 == 0x0010) || (s0 == 0x2001 && seg[1] & 0xfff0 == 0x0020) || (s0 == 0x2001 && seg[1] == 0x0db8) || s0 == 0x2002 || s0 & 0xfe00 == 0xfc00 || s0 & 0xffc0 == 0xfe80 || s0 & 0xff00 == 0xff00 }
pub(crate) fn validate_url(input: &str) -> anyhow::Result<url::Url> {
use anyhow::{Context as _, bail};
let mut parsed = url::Url::parse(input).with_context(|| format!("invalid URL: {input}"))?;
match parsed.scheme() {
"http" | "https" => {}
s => bail!("scheme '{s}' not allowed; only http:// and https:// are supported"),
}
if !parsed.username().is_empty() || parsed.password().is_some() {
eprintln!("warning: credentials stripped from URL");
let _ = parsed.set_username("");
let _ = parsed.set_password(None);
}
if let Some(host) = parsed.host_str() {
if is_private_host(host) {
bail!("access to private/local addresses is not allowed: {host}");
}
}
Ok(parsed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blocks_localhost() {
assert!(is_private_host("localhost"));
assert!(is_private_host("127.0.0.1"));
assert!(is_private_host("[::1]"));
}
#[test]
fn blocks_private_ipv4() {
assert!(is_private_host("10.0.0.1"));
assert!(is_private_host("192.168.1.1"));
assert!(is_private_host("172.16.0.1"));
}
#[test]
fn blocks_shared_cgn() {
assert!(is_private_host("100.64.0.1"));
assert!(is_private_host("100.127.255.254"));
}
#[test]
fn blocks_documentation_ips() {
assert!(is_private_host("192.0.2.1"));
assert!(is_private_host("198.51.100.1"));
assert!(is_private_host("203.0.113.1"));
}
#[test]
fn blocks_multicast() {
assert!(is_private_host("224.0.0.1"));
}
#[test]
fn blocks_metadata() {
assert!(is_private_host("169.254.169.254"));
assert!(is_private_host("metadata.google.internal"));
}
#[test]
fn blocks_ipv4_mapped_ipv6() {
assert!(is_private_host("::ffff:127.0.0.1"));
assert!(is_private_host("::ffff:10.0.0.1"));
}
#[test]
fn blocks_ipv6_special() {
assert!(is_private_host("fe80::1"));
assert!(is_private_host("fd00::1"));
assert!(is_private_host("2001:db8::1"));
}
#[test]
fn allows_public() {
assert!(!is_private_host("8.8.8.8"));
assert!(!is_private_host("1.1.1.1"));
assert!(!is_private_host("example.com"));
}
}