Better IP check (#161)

This commit is contained in:
Nutomic 2026-02-05 12:04:08 +00:00 committed by GitHub
parent f60afae428
commit f47fe58285
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 59 additions and 33 deletions

View file

@ -14,10 +14,6 @@ actix-web = ["dep:actix-web", "dep:http02"]
axum = ["dep:axum", "dep:tower"] axum = ["dep:axum", "dep:tower"]
axum-original-uri = ["dep:axum", "axum/original-uri"] axum-original-uri = ["dep:axum", "axum/original-uri"]
[lints.rust]
warnings = "deny"
deprecated = "deny"
[lints.clippy] [lints.clippy]
perf = { level = "deny", priority = -1 } perf = { level = "deny", priority = -1 }
complexity = { level = "deny", priority = -1 } complexity = { level = "deny", priority = -1 }

View file

@ -20,12 +20,12 @@ use crate::{
http_signatures::sign_request, http_signatures::sign_request,
protocol::verification::verify_domains_match, protocol::verification::verify_domains_match,
traits::{Activity, Actor}, traits::{Activity, Actor},
utils::is_invalid_ip,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use derive_builder::Builder; use derive_builder::Builder;
use dyn_clone::{clone_trait_object, DynClone}; use dyn_clone::{clone_trait_object, DynClone};
use itertools::Itertools;
use moka::future::Cache; use moka::future::Cache;
use regex::Regex; use regex::Regex;
use reqwest::{redirect::Policy, Client, Request}; use reqwest::{redirect::Policy, Client, Request};
@ -33,7 +33,6 @@ use reqwest_middleware::{ClientWithMiddleware, RequestBuilder};
use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey}; use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use std::{ use std::{
net::IpAddr,
ops::Deref, ops::Deref,
sync::{ sync::{
atomic::{AtomicU32, Ordering}, atomic::{AtomicU32, Ordering},
@ -42,7 +41,6 @@ use std::{
}, },
time::Duration, time::Duration,
}; };
use tokio::net::lookup_host;
use url::Url; use url::Url;
/// Configuration for this library, with various federation related settings /// Configuration for this library, with various federation related settings
@ -184,31 +182,9 @@ impl<T: Clone> FederationConfig<T> {
return Err(Error::UrlVerificationError("Explicit port is not allowed")); 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?
.map(|s| s.ip().to_canonical());
let allow_local = std::env::var("DANGER_FEDERATION_ALLOW_LOCAL_IP").is_ok(); let allow_local = std::env::var("DANGER_FEDERATION_ALLOW_LOCAL_IP").is_ok();
let invalid_ip = !allow_local if !allow_local && is_invalid_ip(domain).await? {
&& ips.any(|ip| match ip { return Err(Error::DomainResolveError(domain.to_string()));
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));
} }
} }

View file

@ -29,8 +29,8 @@ pub enum Error {
#[error("URL failed verification: {0}")] #[error("URL failed verification: {0}")]
UrlVerificationError(&'static str), UrlVerificationError(&'static str),
/// Resolving domain points to local IP. /// 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")] #[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, String), DomainResolveError(String),
/// Incoming activity has invalid digest for body /// Incoming activity has invalid digest for body
#[error("Incoming activity has invalid digest for body")] #[error("Incoming activity has invalid digest for body")]
ActivityBodyDigestInvalid, ActivityBodyDigestInvalid,

View file

@ -23,6 +23,7 @@ pub mod http_signatures;
pub mod protocol; pub mod protocol;
pub(crate) mod reqwest_shim; pub(crate) mod reqwest_shim;
pub mod traits; pub mod traits;
mod utils;
use crate::{ use crate::{
config::Data, config::Data,

53
src/utils.rs Normal file
View file

@ -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<bool, Error> {
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(())
}
}