Better IP check
This commit is contained in:
parent
11f95ff384
commit
b1c4d01953
5 changed files with 59 additions and 31 deletions
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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,29 +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?;
|
|
||||||
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(|addr| match addr.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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
53
src/utils.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue