use crate::error::ConfigError;
use url::Url;
fn is_test_mode() -> bool {
if std::env::var("CONFERS_TEST_ALLOW_LOCALHOST").is_ok() {
let env = std::env::var("APP_ENV").unwrap_or_default();
env != "production"
} else {
false
}
}
pub fn validate_remote_url(url: &str) -> Result<(), ConfigError> {
if is_test_mode() {
return Ok(());
}
let parsed_url =
Url::parse(url).map_err(|e| ConfigError::RemoteError(format!("Invalid URL: {}", e)))?;
match parsed_url.scheme() {
"http" | "https" => {}
scheme => {
return Err(ConfigError::RemoteError(format!(
"Only HTTP and HTTPS protocols are allowed, got: {}",
scheme
)))
}
}
if let Some(host) = parsed_url.host_str() {
let host_clean = host.trim_start_matches('[').trim_end_matches(']');
if is_localhost(host_clean) {
return Err(ConfigError::RemoteError(
"Access to localhost is not allowed".to_string(),
));
}
if is_hostname(host_clean) {
check_dns_rebinding(host_clean)?;
} else {
if is_private_ip(host_clean) {
return Err(ConfigError::RemoteError(
"Access to private IP addresses is not allowed".to_string(),
));
}
if is_link_local(host_clean) {
return Err(ConfigError::RemoteError(
"Access to link-local addresses is not allowed".to_string(),
));
}
}
}
Ok(())
}
fn is_hostname(host: &str) -> bool {
host.parse::<std::net::Ipv4Addr>().is_err() && host.parse::<std::net::Ipv6Addr>().is_err()
}
fn check_dns_rebinding(host: &str) -> Result<(), ConfigError> {
use std::net::ToSocketAddrs;
match (host, 0).to_socket_addrs() {
Ok(addrs) => {
for addr in addrs {
let ip = addr.ip();
let ip_str = ip.to_string();
if is_private_ip(&ip_str) {
return Err(ConfigError::RemoteError(format!(
"DNS rebinding detected: {} resolves to private IP {}",
host, ip
)));
}
if is_link_local(&ip_str) {
return Err(ConfigError::RemoteError(format!(
"DNS rebinding detected: {} resolves to link-local IP {}",
host, ip
)));
}
if is_localhost(&ip_str) {
return Err(ConfigError::RemoteError(format!(
"DNS rebinding detected: {} resolves to localhost {}",
host, ip
)));
}
}
}
Err(_e) => {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to resolve hostname {}: {}", host, _e);
}
}
Ok(())
}
fn is_localhost(host: &str) -> bool {
let host_lower = host.to_lowercase();
matches!(
host_lower.as_str(),
"localhost" | "127.0.0.1" | "::1" | "0.0.0.0" | "[::]"
)
}
fn is_private_ip(host: &str) -> bool {
if let Ok(addr) = host.parse::<std::net::Ipv4Addr>() {
return is_private_ipv4(&addr);
}
if let Ok(addr) = host.parse::<std::net::Ipv6Addr>() {
return is_private_ipv6(&addr);
}
false
}
fn is_private_ipv4(addr: &std::net::Ipv4Addr) -> bool {
let octets = addr.octets();
if octets[0] == 10 {
return true;
}
if octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31 {
return true;
}
if octets[0] == 192 && octets[1] == 168 {
return true;
}
if octets[0] == 169 && octets[1] == 254 {
return true;
}
if octets[0] == 100 && (octets[1] & 0b1100_0000) == 0b0100_0000 {
return true;
}
if octets[0] == 192 && octets[1] == 0 && octets[2] == 0 {
return true;
}
if octets[0] == 192 && octets[1] == 0 && octets[2] == 2 {
return true;
}
if octets[0] == 198 && octets[1] == 51 && octets[2] == 100 {
return true;
}
if octets[0] == 203 && octets[1] == 0 && octets[2] == 113 {
return true;
}
if octets[0] & 0b1111_0000 == 0b1111_0000 {
return true;
}
false
}
fn is_private_ipv6(addr: &std::net::Ipv6Addr) -> bool {
let segments = addr.segments();
if segments[0] & 0xfe00 == 0xfc00 {
return true;
}
if segments[0] & 0xffc0 == 0xfe80 {
return true;
}
if addr.is_unspecified() {
return true;
}
if segments[0] & 0xff00 == 0xff00 {
return true;
}
false
}
fn is_link_local(host: &str) -> bool {
if let Ok(addr) = host.parse::<std::net::Ipv4Addr>() {
return is_link_local_ipv4(&addr);
}
if let Ok(addr) = host.parse::<std::net::Ipv6Addr>() {
return is_link_local_ipv6(&addr);
}
false
}
fn is_link_local_ipv4(addr: &std::net::Ipv4Addr) -> bool {
let octets = addr.octets();
octets[0] == 169 && octets[1] == 254
}
fn is_link_local_ipv6(addr: &std::net::Ipv6Addr) -> bool {
let segments = addr.segments();
segments[0] & 0xffc0 == 0xfe80
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_public_url() {
assert!(validate_remote_url("https://example.com").is_ok());
assert!(validate_remote_url("http://api.example.com:8080").is_ok());
}
#[test]
fn test_block_localhost() {
assert!(validate_remote_url("http://localhost").is_err());
assert!(validate_remote_url("http://127.0.0.1").is_err());
assert!(validate_remote_url("http://127.0.0.1:8080").is_err());
assert!(validate_remote_url("http://[::1]").is_err());
}
#[test]
fn test_block_private_ipv4() {
assert!(validate_remote_url("http://10.0.0.1").is_err());
assert!(validate_remote_url("http://10.255.255.255").is_err());
assert!(validate_remote_url("http://172.16.0.1").is_err());
assert!(validate_remote_url("http://172.31.255.255").is_err());
assert!(validate_remote_url("http://192.168.1.1").is_err());
assert!(validate_remote_url("http://192.168.255.255").is_err());
}
#[test]
fn test_block_private_ipv6() {
assert!(validate_remote_url("http://[fc00::1]").is_err());
assert!(validate_remote_url("http://[fd00::1]").is_err());
assert!(validate_remote_url("http://[fe80::1]").is_err());
}
#[test]
fn test_block_link_local() {
assert!(validate_remote_url("http://169.254.1.1").is_err());
assert!(validate_remote_url("http://[fe80::1]").is_err());
}
#[test]
fn test_block_invalid_protocol() {
assert!(validate_remote_url("ftp://example.com").is_err());
assert!(validate_remote_url("file:///etc/passwd").is_err());
}
#[test]
fn test_block_unspecified() {
assert!(validate_remote_url("http://0.0.0.0").is_err());
assert!(validate_remote_url("http://[::]").is_err());
}
}