use crate::error::{FeedError, Result};
#[cfg(feature = "http")]
use std::net::IpAddr;
use std::net::{Ipv4Addr, Ipv6Addr};
use url::Url;
const LOCALHOST_VARIANTS: &[&str] = &[
"localhost",
"localhost.localdomain",
"127.0.0.1",
"::1",
"[::1]",
];
const INTERNAL_TLDS: &[&str] = &[
".local",
".localhost",
".internal",
".intranet",
".corp",
".home",
".lan",
];
const METADATA_DOMAINS: &[&str] = &[
"metadata.google.internal",
"169.254.169.254",
"metadata",
"metadata.azure.com",
];
pub fn validate_url(url_str: &str) -> Result<Url> {
let url = Url::parse(url_str).map_err(|e| FeedError::Http {
message: format!("Invalid URL: {e}"),
})?;
match url.scheme() {
"http" | "https" => {}
scheme => {
return Err(FeedError::Http {
message: format!(
"Unsupported URL scheme '{scheme}': only 'http' and 'https' are allowed"
),
});
}
}
let host = url.host().ok_or_else(|| FeedError::Http {
message: "URL must have a host".to_string(),
})?;
match host {
url::Host::Ipv4(ip) => validate_ipv4(ip)?,
url::Host::Ipv6(ip) => validate_ipv6(ip)?,
url::Host::Domain(domain) => validate_domain(domain)?,
}
Ok(url)
}
#[cfg(feature = "http")]
pub fn validate_ip_addr(ip: IpAddr) -> Result<()> {
match ip {
IpAddr::V4(ip) => validate_ipv4(ip),
IpAddr::V6(ip) => validate_ipv6(ip),
}
}
fn validate_ipv4(ip: Ipv4Addr) -> Result<()> {
if ip.is_private() {
return Err(FeedError::Http {
message: format!("Private IP address not allowed: {ip} (RFC 1918)"),
});
}
if ip.is_loopback() {
return Err(FeedError::Http {
message: format!("Loopback address not allowed: {ip}"),
});
}
if ip.is_link_local() {
return Err(FeedError::Http {
message: format!("Link-local address not allowed: {ip} (169.254.0.0/16)"),
});
}
if ip.is_broadcast() {
return Err(FeedError::Http {
message: format!("Broadcast address not allowed: {ip}"),
});
}
if ip.is_documentation() {
return Err(FeedError::Http {
message: format!("Documentation IP not allowed: {ip} (RFC 5737)"),
});
}
if ip.is_multicast() {
return Err(FeedError::Http {
message: format!("Multicast address not allowed: {ip} (224.0.0.0/4)"),
});
}
validate_ipv4_special_purpose(ip, ip.octets())
}
fn validate_ipv4_special_purpose(ip: Ipv4Addr, octets: [u8; 4]) -> Result<()> {
if octets[0] >= 240 {
return Err(FeedError::Http {
message: format!("Reserved address not allowed: {ip} (240.0.0.0/4)"),
});
}
if octets[0] == 192 && octets[1] == 0 && octets[2] == 0 {
return Err(FeedError::Http {
message: format!("IETF protocol assignment address not allowed: {ip} (192.0.0.0/24)"),
});
}
if octets[0] == 192 && octets[1] == 88 && octets[2] == 99 {
return Err(FeedError::Http {
message: format!("6to4 relay anycast address not allowed: {ip} (192.88.99.0/24)"),
});
}
if octets[0] == 198 && (octets[1] & 0xFE) == 18 {
return Err(FeedError::Http {
message: format!("Benchmarking address not allowed: {ip} (198.18.0.0/15)"),
});
}
if octets[0] == 169 && octets[1] == 254 && octets[2] == 169 && octets[3] == 254 {
return Err(FeedError::Http {
message: "AWS metadata endpoint blocked: 169.254.169.254".to_string(),
});
}
if octets[0] == 100 && (octets[1] & 0xC0) == 64 {
return Err(FeedError::Http {
message: format!("Carrier-grade NAT address not allowed: {ip} (100.64.0.0/10)"),
});
}
if octets[0] == 192 && octets[1] == 31 && octets[2] == 196 {
return Err(FeedError::Http {
message: format!("AS112-v4 address not allowed: {ip} (192.31.196.0/24)"),
});
}
if octets[0] == 192 && octets[1] == 52 && octets[2] == 193 {
return Err(FeedError::Http {
message: format!("AMT address not allowed: {ip} (192.52.193.0/24)"),
});
}
if octets[0] == 192 && octets[1] == 175 && octets[2] == 48 {
return Err(FeedError::Http {
message: format!(
"Direct Delegation AS112 Service address not allowed: {ip} (192.175.48.0/24)"
),
});
}
if octets[0] == 0 {
return Err(FeedError::Http {
message: format!("0.0.0.0/8 range not allowed: {ip}"),
});
}
Ok(())
}
const NAT64_PREFIX: [u16; 6] = [0x0064, 0xff9b, 0, 0, 0, 0];
const IPV4_COMPATIBLE_PREFIX: [u16; 6] = [0, 0, 0, 0, 0, 0];
const fn embedded_ipv4(segments: [u16; 8]) -> Ipv4Addr {
Ipv4Addr::new(
(segments[6] >> 8) as u8,
(segments[6] & 0xFF) as u8,
(segments[7] >> 8) as u8,
(segments[7] & 0xFF) as u8,
)
}
fn validate_ipv6(ip: Ipv6Addr) -> Result<()> {
if let Some(mapped) = ip.to_ipv4_mapped() {
return validate_ipv4(mapped);
}
let segments = ip.segments();
let low32 = (u32::from(segments[6]) << 16) | u32::from(segments[7]);
if segments[..6] == IPV4_COMPATIBLE_PREFIX && low32 > 1 {
return validate_ipv4(embedded_ipv4(segments));
}
if segments[..6] == NAT64_PREFIX {
return validate_ipv4(embedded_ipv4(segments));
}
if ip.is_loopback() {
return Err(FeedError::Http {
message: format!("IPv6 loopback address not allowed: {ip}"),
});
}
if ip.is_unspecified() {
return Err(FeedError::Http {
message: format!("IPv6 unspecified address not allowed: {ip}"),
});
}
if ip.is_unicast_link_local() {
return Err(FeedError::Http {
message: format!("IPv6 link-local address not allowed: {ip} (fe80::/10)"),
});
}
if (segments[0] & 0xFE00) == 0xFC00 {
return Err(FeedError::Http {
message: format!("IPv6 unique local address not allowed: {ip} (fc00::/7)"),
});
}
validate_ipv6_special_purpose(ip, segments)?;
validate_ipv6_special_purpose_extended(ip, segments)?;
if ip.is_multicast() {
return Err(FeedError::Http {
message: format!("IPv6 multicast address not allowed: {ip} (ff00::/8)"),
});
}
Ok(())
}
fn validate_ipv6_special_purpose(ip: Ipv6Addr, segments: [u16; 8]) -> Result<()> {
if segments[0] == 0x2001 && (segments[1] & 0xFE00) == 0 {
return Err(FeedError::Http {
message: format!(
"IPv6 IETF Protocol Assignments address not allowed: {ip} (2001::/23, RFC 6890)"
),
});
}
if segments[0] == 0x2001 && segments[1] == 0x0db8 {
return Err(FeedError::Http {
message: format!("IPv6 documentation address not allowed: {ip} (2001:db8::/32)"),
});
}
if segments[0] == 0x0100 && segments[1] == 0 && segments[2] == 0 && segments[3] == 0 {
return Err(FeedError::Http {
message: format!("IPv6 discard-only address not allowed: {ip} (100::/64)"),
});
}
if segments[0] == 0x0100 && segments[1] == 0 && segments[2] == 0 && segments[3] == 1 {
return Err(FeedError::Http {
message: format!("IPv6 dummy prefix address not allowed: {ip} (100:0:0:1::/64)"),
});
}
if segments[0] == 0x0064 && segments[1] == 0xff9b && segments[2] == 0x0001 {
return Err(FeedError::Http {
message: format!("IPv6 NAT64 local-use address not allowed: {ip} (64:ff9b:1::/48)"),
});
}
if segments[0] == 0x2620 && segments[1] == 0x004f && segments[2] == 0x8000 {
return Err(FeedError::Http {
message: format!(
"IPv6 Direct Delegation AS112 Service address not allowed: {ip} (2620:4f:8000::/48)"
),
});
}
Ok(())
}
fn validate_ipv6_special_purpose_extended(ip: Ipv6Addr, segments: [u16; 8]) -> Result<()> {
if segments[0] == 0x3fff && (segments[1] & 0xF000) == 0 {
return Err(FeedError::Http {
message: format!("IPv6 documentation address not allowed: {ip} (3fff::/20)"),
});
}
if segments[0] == 0x5f00 {
return Err(FeedError::Http {
message: format!("IPv6 segment routing address not allowed: {ip} (5f00::/16)"),
});
}
Ok(())
}
fn validate_domain(domain: &str) -> Result<()> {
let domain_lower = domain.to_lowercase();
let normalized = domain_lower.trim_end_matches('.');
if LOCALHOST_VARIANTS.contains(&normalized) {
return Err(FeedError::Http {
message: format!("Localhost domain not allowed: {domain}"),
});
}
for tld in INTERNAL_TLDS {
if normalized.ends_with(tld) {
return Err(FeedError::Http {
message: format!("Internal domain TLD not allowed: {domain}"),
});
}
}
if METADATA_DOMAINS.contains(&normalized) {
return Err(FeedError::Http {
message: format!("Cloud metadata domain not allowed: {domain}"),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
#[test]
fn test_validate_url_public_ok() {
assert!(validate_url("https://example.com/feed.xml").is_ok());
}
#[test]
fn test_validate_url_rejects_private_ip() {
assert!(validate_url("http://10.0.0.1/").is_err());
}
#[test]
#[cfg(feature = "http")]
fn test_validate_ip_addr_rejects_loopback() {
assert!(validate_ip_addr(IpAddr::V4(Ipv4Addr::LOCALHOST)).is_err());
}
#[test]
#[cfg(feature = "http")]
fn test_validate_ip_addr_rejects_metadata() {
assert!(validate_ip_addr(IpAddr::V4(Ipv4Addr::new(169, 254, 169, 254))).is_err());
}
#[test]
#[cfg(feature = "http")]
fn test_validate_ip_addr_accepts_public() {
assert!(validate_ip_addr(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))).is_ok());
}
#[test]
#[cfg(feature = "http")]
fn test_validate_ip_addr_rejects_ipv6_unspecified() {
assert!(validate_ip_addr(IpAddr::V6(Ipv6Addr::UNSPECIFIED)).is_err());
}
#[test]
fn test_validate_url_rejects_ipv4_mapped_loopback() {
assert!(validate_url("http://[::ffff:127.0.0.1]/").is_err());
}
#[test]
fn test_validate_url_rejects_ipv4_mapped_private() {
assert!(validate_url("http://[::ffff:10.0.0.1]/").is_err());
}
#[test]
fn test_validate_url_allows_ipv4_mapped_public() {
assert!(validate_url("http://[::ffff:8.8.8.8]/").is_ok());
}
#[test]
fn test_validate_url_rejects_ipv4_compatible_loopback() {
assert!(validate_url("http://[::127.0.0.1]/").is_err());
}
#[test]
fn test_validate_url_rejects_ipv4_compatible_metadata() {
assert!(validate_url("http://[::169.254.169.254]/").is_err());
}
#[test]
fn test_validate_url_allows_ipv4_compatible_public() {
assert!(validate_url("http://[::8.8.8.8]/").is_ok());
}
#[test]
fn test_validate_url_rejects_nat64_metadata() {
assert!(validate_url("http://[64:ff9b::a9fe:a9fe]/").is_err());
}
#[test]
fn test_validate_url_rejects_nat64_private() {
assert!(validate_url("http://[64:ff9b::a00:1]/").is_err());
}
#[test]
fn test_validate_url_allows_nat64_public() {
assert!(validate_url("http://[64:ff9b::808:808]/").is_ok());
}
#[test]
fn test_validate_domain_rejects_trailing_dot_metadata_google() {
assert!(validate_domain("metadata.google.internal.").is_err());
}
#[test]
fn test_validate_domain_rejects_trailing_dot_localhost() {
assert!(validate_domain("localhost.").is_err());
}
#[test]
fn test_validate_domain_rejects_trailing_dot_metadata() {
assert!(validate_domain("metadata.").is_err());
}
#[test]
fn test_validate_domain_rejects_trailing_dot_internal_tld() {
assert!(validate_domain("server.local.").is_err());
assert!(validate_domain("server.internal.").is_err());
}
#[test]
fn test_validate_domain_allows_trailing_dot_public() {
assert!(validate_domain("example.com.").is_ok());
}
#[test]
fn test_validate_domain_rejects_multiple_trailing_dots() {
assert!(validate_domain("localhost..").is_err());
assert!(validate_domain("metadata.google.internal..").is_err());
assert!(validate_domain("server.internal..").is_err());
}
#[test]
fn test_validate_url_rejects_trailing_dot_metadata_google() {
assert!(validate_url("http://metadata.google.internal./").is_err());
}
#[test]
fn test_validate_ipv4_rejects_multicast_boundaries() {
assert!(validate_ipv4(Ipv4Addr::new(224, 0, 0, 0)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(239, 255, 255, 255)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(223, 255, 255, 255)).is_ok());
}
#[test]
fn test_validate_ipv4_rejects_reserved_boundaries() {
assert!(validate_ipv4(Ipv4Addr::new(240, 0, 0, 0)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(255, 255, 255, 254)).is_err());
}
#[test]
fn test_validate_ipv4_rejects_ietf_protocol_assignment_boundaries() {
assert!(validate_ipv4(Ipv4Addr::new(192, 0, 0, 0)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(192, 0, 0, 255)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(191, 255, 255, 255)).is_ok());
assert!(validate_ipv4(Ipv4Addr::new(192, 0, 1, 0)).is_ok());
}
#[test]
fn test_validate_ipv4_rejects_6to4_relay_anycast_boundaries() {
assert!(validate_ipv4(Ipv4Addr::new(192, 88, 99, 0)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(192, 88, 99, 255)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(192, 88, 98, 255)).is_ok());
assert!(validate_ipv4(Ipv4Addr::new(192, 88, 100, 0)).is_ok());
}
#[test]
fn test_validate_ipv4_rejects_benchmarking_boundaries() {
assert!(validate_ipv4(Ipv4Addr::new(198, 18, 0, 0)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(198, 19, 255, 255)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(198, 17, 255, 255)).is_ok());
assert!(validate_ipv4(Ipv4Addr::new(198, 20, 0, 0)).is_ok());
}
#[test]
fn test_validate_ipv6_rejects_ietf_protocol_assignments_2001_23_boundaries() {
assert!(validate_ipv6(Ipv6Addr::new(0x2001, 0, 0, 0, 0, 0, 0, 0)).is_err());
assert!(
validate_ipv6(Ipv6Addr::new(
0x2001, 0x01ff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff
))
.is_err()
);
assert!(
validate_ipv6(Ipv6Addr::new(
0x2000, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff
))
.is_ok()
);
assert!(validate_ipv6(Ipv6Addr::new(0x2001, 0x0200, 0, 0, 0, 0, 0, 0)).is_ok());
assert!(validate_ipv6(Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 0)).is_err());
assert!(validate_ipv6(Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 1)).is_err());
assert!(validate_ipv6(Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 2)).is_err());
assert!(validate_ipv6(Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 3)).is_err());
assert!(validate_ipv6(Ipv6Addr::new(0x2001, 2, 0, 0, 0, 0, 0, 0)).is_err());
assert!(validate_ipv6(Ipv6Addr::new(0x2001, 3, 0, 0, 0, 0, 0, 0)).is_err());
assert!(validate_ipv6(Ipv6Addr::new(0x2001, 4, 0x0112, 0, 0, 0, 0, 0)).is_err());
assert!(validate_ipv6(Ipv6Addr::new(0x2001, 0x0010, 0, 0, 0, 0, 0, 0)).is_err());
assert!(validate_ipv6(Ipv6Addr::new(0x2001, 0x0020, 0, 0, 0, 0, 0, 0)).is_err());
assert!(validate_ipv6(Ipv6Addr::new(0x2001, 0x0030, 0, 0, 0, 0, 0, 0)).is_err());
}
#[test]
fn test_validate_ipv6_rejects_documentation_boundaries() {
assert!(validate_ipv6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0)).is_err());
assert!(
validate_ipv6(Ipv6Addr::new(
0x2001, 0x0db8, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff
))
.is_err()
);
assert!(
validate_ipv6(Ipv6Addr::new(
0x2001, 0x0db7, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff
))
.is_ok()
);
assert!(validate_ipv6(Ipv6Addr::new(0x2001, 0x0db9, 0, 0, 0, 0, 0, 0)).is_ok());
}
#[test]
fn test_validate_ipv6_rejects_discard_only_boundaries() {
assert!(validate_ipv6(Ipv6Addr::new(0x0100, 0, 0, 0, 0, 0, 0, 0)).is_err());
assert!(
validate_ipv6(Ipv6Addr::new(
0x0100, 0, 0, 0, 0xffff, 0xffff, 0xffff, 0xffff
))
.is_err()
);
assert!(
validate_ipv6(Ipv6Addr::new(
0x00ff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff
))
.is_ok()
);
assert!(validate_ipv6(Ipv6Addr::new(0x0100, 0, 0, 1, 0, 0, 0, 0)).is_err());
assert!(validate_ipv6(Ipv6Addr::new(0x0100, 0, 0, 2, 0, 0, 0, 0)).is_ok());
}
#[test]
fn test_validate_ipv6_rejects_nat64_local_use_boundaries() {
assert!(validate_ipv6(Ipv6Addr::new(0x0064, 0xff9b, 1, 0, 0, 0, 0, 0)).is_err());
assert!(
validate_ipv6(Ipv6Addr::new(
0x0064, 0xff9b, 1, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff
))
.is_err()
);
assert!(
validate_ipv6(Ipv6Addr::new(
0x0064, 0xff9b, 0, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff
))
.is_ok()
);
assert!(validate_ipv6(Ipv6Addr::new(0x0064, 0xff9b, 2, 0, 0, 0, 0, 0)).is_ok());
}
#[test]
fn test_validate_ipv6_allows_6to4_deliberately_out_of_scope() {
assert!(validate_ipv6(Ipv6Addr::new(0x2002, 0, 0, 0, 0, 0, 0, 0)).is_ok());
assert!(validate_ipv6(Ipv6Addr::new(0x2002, 0x0a00, 0x0001, 0, 0, 0, 0, 0)).is_ok());
}
#[test]
fn test_validate_ipv4_rejects_as112_v4_boundaries() {
assert!(validate_ipv4(Ipv4Addr::new(192, 31, 196, 0)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(192, 31, 196, 255)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(192, 31, 195, 255)).is_ok());
assert!(validate_ipv4(Ipv4Addr::new(192, 31, 197, 0)).is_ok());
}
#[test]
fn test_validate_ipv4_rejects_amt_boundaries() {
assert!(validate_ipv4(Ipv4Addr::new(192, 52, 193, 0)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(192, 52, 193, 255)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(192, 52, 192, 255)).is_ok());
assert!(validate_ipv4(Ipv4Addr::new(192, 52, 194, 0)).is_ok());
}
#[test]
fn test_validate_ipv4_rejects_direct_delegation_as112_boundaries() {
assert!(validate_ipv4(Ipv4Addr::new(192, 175, 48, 0)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(192, 175, 48, 255)).is_err());
assert!(validate_ipv4(Ipv4Addr::new(192, 175, 47, 255)).is_ok());
assert!(validate_ipv4(Ipv4Addr::new(192, 175, 49, 0)).is_ok());
}
#[test]
fn test_validate_ipv6_rejects_dummy_prefix_boundaries() {
assert!(validate_ipv6(Ipv6Addr::new(0x0100, 0, 0, 1, 0, 0, 0, 0)).is_err());
assert!(
validate_ipv6(Ipv6Addr::new(
0x0100, 0, 0, 1, 0xffff, 0xffff, 0xffff, 0xffff
))
.is_err()
);
assert!(validate_ipv6(Ipv6Addr::new(0x0100, 0, 0, 2, 0, 0, 0, 0)).is_ok());
}
#[test]
fn test_validate_ipv6_rejects_documentation_3fff_boundaries() {
assert!(validate_ipv6(Ipv6Addr::new(0x3fff, 0, 0, 0, 0, 0, 0, 0)).is_err());
assert!(
validate_ipv6(Ipv6Addr::new(
0x3fff, 0x0fff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff
))
.is_err()
);
assert!(validate_ipv6(Ipv6Addr::new(0x3fff, 0x1000, 0, 0, 0, 0, 0, 0)).is_ok());
assert!(validate_ipv6(Ipv6Addr::new(0x3ffe, 0xffff, 0, 0, 0, 0, 0, 0)).is_ok());
}
#[test]
fn test_validate_ipv6_rejects_segment_routing_5f00_boundaries() {
assert!(validate_ipv6(Ipv6Addr::new(0x5f00, 0, 0, 0, 0, 0, 0, 0)).is_err());
assert!(
validate_ipv6(Ipv6Addr::new(
0x5f00, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff
))
.is_err()
);
assert!(validate_ipv6(Ipv6Addr::new(0x5eff, 0xffff, 0, 0, 0, 0, 0, 0)).is_ok());
assert!(validate_ipv6(Ipv6Addr::new(0x5f01, 0, 0, 0, 0, 0, 0, 0)).is_ok());
}
#[test]
fn test_validate_ipv6_rejects_direct_delegation_as112_boundaries() {
assert!(validate_ipv6(Ipv6Addr::new(0x2620, 0x004f, 0x8000, 0, 0, 0, 0, 0)).is_err());
assert!(
validate_ipv6(Ipv6Addr::new(
0x2620, 0x004f, 0x8000, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff
))
.is_err()
);
assert!(validate_ipv6(Ipv6Addr::new(0x2620, 0x004f, 0x7fff, 0, 0, 0, 0, 0)).is_ok());
assert!(validate_ipv6(Ipv6Addr::new(0x2620, 0x004f, 0x8001, 0, 0, 0, 0, 0)).is_ok());
}
}