diff --git a/Cargo.toml b/Cargo.toml index 985e754..bf6952f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,6 @@ actix-web = ["dep:actix-web", "dep:http02"] axum = ["dep:axum", "dep:tower"] axum-original-uri = ["dep:axum", "axum/original-uri"] -[lints.rust] -warnings = "deny" -deprecated = "deny" - [lints.clippy] perf = { level = "deny", priority = -1 } complexity = { level = "deny", priority = -1 } diff --git a/src/config.rs b/src/config.rs index a7cbf94..c5e26f1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,12 +20,12 @@ use crate::{ http_signatures::sign_request, protocol::verification::verify_domains_match, traits::{Activity, Actor}, + utils::is_invalid_ip, }; use async_trait::async_trait; use bytes::Bytes; use derive_builder::Builder; use dyn_clone::{clone_trait_object, DynClone}; -use itertools::Itertools; use moka::future::Cache; use regex::Regex; use reqwest::{redirect::Policy, Client, Request}; @@ -33,7 +33,6 @@ use reqwest_middleware::{ClientWithMiddleware, RequestBuilder}; use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey}; use serde::de::DeserializeOwned; use std::{ - net::IpAddr, ops::Deref, sync::{ atomic::{AtomicU32, Ordering}, @@ -42,7 +41,6 @@ use std::{ }, time::Duration, }; -use tokio::net::lookup_host; use url::Url; /// Configuration for this library, with various federation related settings @@ -184,29 +182,9 @@ impl FederationConfig { return Err(Error::UrlVerificationError("Explicit port is not allowed")); } - // Resolve domain and see if it points to private IP - // TODO: Use is_global() once stabilized - // https://doc.rust-lang.org/std/net/enum.IpAddr.html#method.is_global - let mut ips = lookup_host((domain.to_owned(), 80)).await?; let allow_local = std::env::var("DANGER_FEDERATION_ALLOW_LOCAL_IP").is_ok(); - let invalid_ip = !allow_local - && ips.any(|addr| match addr.ip() { - IpAddr::V4(addr) => { - addr.is_private() - || addr.is_link_local() - || addr.is_loopback() - || addr.is_multicast() - } - IpAddr::V6(addr) => { - addr.is_loopback() - || addr.is_multicast() - || ((addr.segments()[0] & 0xfe00) == 0xfc00) // is_unique_local - || ((addr.segments()[0] & 0xffc0) == 0xfe80) // is_unicast_link_local - } - }); - if invalid_ip { - let ip_addrs = ips.join(", "); - return Err(Error::DomainResolveError(domain.to_string(), ip_addrs)); + if !allow_local && is_invalid_ip(domain).await? { + return Err(Error::DomainResolveError(domain.to_string())); } } diff --git a/src/error.rs b/src/error.rs index 07e377b..e7bac0a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,8 +29,8 @@ pub enum Error { #[error("URL failed verification: {0}")] UrlVerificationError(&'static str), /// Resolving domain points to local IP. - #[error("Resolving domain {0} points to local IP {1}. This may indicate an attacker attempting to access internal services. If intentional, you can ignore this error by setting DANGER_FEDERATION_ALLOW_LOCAL_IP=1")] - DomainResolveError(String, String), + #[error("Resolving domain {0} points to local IP address. This may indicate an attacker attempting to access internal services. If intentional, you can ignore this error by setting DANGER_FEDERATION_ALLOW_LOCAL_IP=1")] + DomainResolveError(String), /// Incoming activity has invalid digest for body #[error("Incoming activity has invalid digest for body")] ActivityBodyDigestInvalid, diff --git a/src/lib.rs b/src/lib.rs index ba8923a..a5d075f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ pub mod http_signatures; pub mod protocol; pub(crate) mod reqwest_shim; pub mod traits; +mod utils; use crate::{ config::Data, diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..d922439 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,53 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use crate::error::Error; +use tokio::net::lookup_host; + +// Resolve domain and see if it points to private IP +// TODO: Use is_global() once stabilized +// https://doc.rust-lang.org/std/net/enum.IpAddr.html#method.is_global +pub(crate) async fn is_invalid_ip(domain: &str) -> Result { + let mut ips = lookup_host((domain, 80)).await?; + Ok(ips.any(|addr| match addr.ip() { + IpAddr::V4(addr) => v4_is_invalid(addr), + IpAddr::V6(addr) => v6_is_invalid(addr), + })) +} + +fn v4_is_invalid(v4: Ipv4Addr) -> bool { + v4.is_private() + || v4.is_loopback() + || v4.is_link_local() + || v4.is_multicast() + || v4.is_documentation() +} + +fn v6_is_invalid(v6: Ipv6Addr) -> bool { + v6.is_loopback() + || v6.is_multicast() + || v6.is_unique_local() + || v6.is_unicast_link_local() + || v6.is_unspecified() + || v6_is_documentation(v6) + || v6.to_ipv4_mapped().is_some_and(v4_is_invalid) +} + +fn v6_is_documentation(v6: std::net::Ipv6Addr) -> bool { + matches!( + v6.segments(), + [0x2001, 0xdb8, ..] | [0x3fff, 0..=0x0fff, ..] + ) +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod test { + use super::*; + + #[tokio::test] + async fn test_is_valid_ip() -> Result<(), Error> { + assert!(!is_invalid_ip("example.com").await?); + assert!(is_invalid_ip("localhost").await?); + Ok(()) + } +}