use crate::error::Error;
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
use std::{
env,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
};
use tokio::net::lookup_host;
use url::{Host, Url};
async fn resolve_host(host: &str, port: Option<u16>) -> Result<Vec<SocketAddr>, Error> {
let host = normalize_host(host);
let default_port = port.unwrap_or(0);
let addrs: Vec<SocketAddr> = match host.parse::<IpAddr>() {
Ok(ip) => vec![SocketAddr::new(ip.to_canonical(), default_port)],
Err(_) => lookup_host((host.as_str(), default_port)).await?.collect(),
};
Ok(addrs)
}
async fn resolve_and_validate_host(
host: &str,
port: Option<u16>,
allow_hosts: &[(String, u16)],
) -> Result<Vec<SocketAddr>, Error> {
let addrs = resolve_host(host, port).await?;
let host = normalize_host(host);
let is_allowed_host = allow_hosts.iter().any(|(allowed_host, allowed_port)| {
allowed_host == &host && (port.is_none() || Some(allowed_port) == port.as_ref())
});
if env::var("DANGER_FEDERATION_ALLOW_LOCAL_IP").is_ok() || is_allowed_host {
return Ok(addrs);
}
if let Some(ip) = addrs
.iter()
.map(|a| a.ip().to_canonical())
.find(|ip| match ip {
IpAddr::V4(ipv4) => v4_is_invalid(*ipv4),
IpAddr::V6(ipv6) => v6_is_invalid(*ipv6),
})
{
return Err(Error::ResolveInternalIpError(host, ip));
}
Ok(addrs)
}
pub(crate) async fn validate_url(url: &Url, allow_hosts: &[Url]) -> Result<(), Error> {
let host = match url
.host()
.ok_or(Error::UrlVerificationError("Url must have a domain"))?
{
Host::Domain(domain) => domain.to_owned(),
Host::Ipv4(ipv4) => ipv4.to_string(),
Host::Ipv6(ipv6) => ipv6.to_string(),
};
resolve_and_validate_host(
&host,
url.port_or_known_default(),
&allow_host_pairs(allow_hosts),
)
.await?;
Ok(())
}
fn allow_host_pairs(allow_hosts: &[Url]) -> Vec<(String, u16)> {
allow_hosts
.iter()
.filter_map(|url| {
let host = normalize_host(url.host_str()?);
Some((host, url.port_or_known_default().unwrap_or(80)))
})
.collect()
}
pub struct ValidatingDnsResolver {
allow_hosts: Vec<(String, u16)>,
validate: bool,
}
impl ValidatingDnsResolver {
pub fn new(allow_hosts: Vec<Url>, validate: bool) -> Self {
Self {
allow_hosts: allow_host_pairs(&allow_hosts),
validate,
}
}
fn allows(&self, host: &str) -> bool {
let host = normalize_host(host);
self.allow_hosts.iter().any(|(h, _)| h == &host)
}
}
impl Resolve for ValidatingDnsResolver {
fn resolve(&self, name: Name) -> Resolving {
let host = name.as_str().trim_end_matches('.').to_owned();
let validate = self.validate && !self.allows(&host);
let allow_hosts = self.allow_hosts.clone();
Box::pin(async move {
let addrs: Vec<SocketAddr> = if validate {
resolve_and_validate_host(&host, None, &allow_hosts).await?
} else {
resolve_host(&host, None).await?
};
let addrs: Addrs = Box::new(addrs.into_iter());
Ok(addrs)
})
}
}
fn normalize_host(host: &str) -> String {
let host = host.trim_end_matches('.').to_ascii_lowercase();
if let Some(stripped) = host.strip_prefix('[') {
if let Some((ipv6, _)) = stripped.split_once(']') {
return ipv6.to_owned();
}
}
if host.matches(':').count() > 1 {
return host;
}
host.split(':').next().unwrap_or(&host).to_owned()
}
fn v4_is_invalid(v4: Ipv4Addr) -> bool {
let octets = v4.octets();
v4.is_private()
|| v4.is_loopback()
|| v4.is_link_local()
|| v4.is_multicast()
|| v4.is_documentation()
|| v4.is_unspecified()
|| v4.is_broadcast()
|| octets[0] == 0
|| (octets[0] == 100 && (octets[1] & 0xc0) == 0x40)
|| octets[0] >= 240 }
fn v6_is_invalid(v6: Ipv6Addr) -> bool {
let segments = v6.segments();
if matches!(segments, [0x64, 0xff9b, 0, 0, 0, 0, ..]) {
let [a, b] = segments[6].to_be_bytes();
let [c, d] = segments[7].to_be_bytes();
return v4_is_invalid(Ipv4Addr::from([a, b, c, d]));
}
matches!(
segments,
[0x2001, 0xdb8, ..] | [0x3fff, 0..=0x0fff, ..]
| [0, 0, 0, 0, 0, 0, ..]
| [0, 0, 0, 0, 0, 0xffff, ..]
| [0x64, 0xff9b, 1, ..]
| [0x2002, ..]
) || v6.is_loopback()
|| v6.is_multicast()
|| v6.is_unique_local()
|| v6.is_unicast_link_local()
|| v6.is_unspecified()
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use super::*;
#[tokio::test]
async fn test_validate_url() -> Result<(), Error> {
assert!(validate_url(&Url::parse("http://example.com")?, &[])
.await
.is_ok());
assert!(validate_url(&Url::parse("http://172.66.147.243")?, &[])
.await
.is_ok());
assert!(validate_url(&Url::parse("http://localhost")?, &[])
.await
.is_err());
assert!(validate_url(&Url::parse("http://127.0.0.1")?, &[])
.await
.is_err());
let allow_hosts = vec![Url::parse("http://localhost:8080")?];
let url = Url::parse("http://localhost:8080/")?;
assert!(validate_url(&url, &allow_hosts).await.is_ok());
let url = Url::parse("http://localhost:9000/")?;
assert!(validate_url(&url, &allow_hosts).await.is_err());
let url = Url::parse("http://localhost/")?;
assert!(validate_url(&url, &allow_hosts).await.is_err());
let url = Url::parse("http://127.0.0.1:8080/")?;
assert!(validate_url(&url, &allow_hosts).await.is_err());
Ok(())
}
#[test]
fn test_invalid_ipv4() {
for ip in [
"0.0.0.1", "10.0.0.1", "100.64.0.1", "100.127.255.254",
"127.0.0.1", "169.254.1.1", "172.16.0.1", "192.0.2.1", "224.0.0.1", "240.0.0.1", "255.255.255.255", ] {
assert!(v4_is_invalid(ip.parse().unwrap()), "{ip}");
}
}
#[test]
fn test_valid_ipv4() {
for ip in [
"1.2.3.4",
"8.8.8.8",
"100.128.0.1", "192.0.0.9",
"192.0.0.10",
"192.0.0.1", "192.88.99.1", "198.18.0.1", ] {
assert!(!v4_is_invalid(ip.parse().unwrap()), "{ip}");
}
}
#[test]
fn test_invalid_ipv6() {
for ip in [
"::1", "fc00::1", "fe80::1", "ff01::1", "2001:db8::1", "3fff:10::1", "::ffff:127.0.0.1", "::7f00:1", "64:ff9b::7f00:1", "64:ff9b::10.0.0.1",
"64:ff9b:1::1", "2002:7f00:1::", "2002:c0a8:101::", ] {
assert!(v6_is_invalid(ip.parse().unwrap()), "{ip}");
}
}
#[test]
fn test_valid_ipv6() {
for ip in [
"2606:4700::1",
"64:ff9b::801:1010",
"2001:1::1",
"2001:3::1",
"2001:20::1",
"2001::1", "2001:100::1", "100::1", "5f00::1", ] {
assert!(!v6_is_invalid(ip.parse().unwrap()), "{ip}");
}
}
#[test]
fn test_normalize_host() {
assert_eq!("example.com", normalize_host("example.com"));
assert_eq!("example.com", normalize_host("EXAMPLE.COM."));
assert_eq!("localhost", normalize_host("localhost:8080"));
assert_eq!("::1", normalize_host("[::1]:8080"));
assert_eq!("::1", normalize_host("[::1]"));
assert_eq!("::1", normalize_host("::1"));
assert_eq!("2001:db8::1", normalize_host("2001:DB8::1."));
}
#[test]
fn test_resolver_allows() {
let resolver = ValidatingDnsResolver::new(
vec![
Url::parse("http://localhost:8080").unwrap(),
Url::parse("https://lemmy.example.com").unwrap(),
],
true,
);
assert!(resolver.allows("localhost"));
assert!(resolver.allows("lemmy.example.com"));
assert!(resolver.allows("lemmy.example.com."));
assert!(!resolver.allows("internal.attacker.com"));
let resolver = ValidatingDnsResolver::new(vec![], false);
assert!(!resolver.allows("internal.attacker.com"));
}
}