From bc3f3fe34c5ac508d9fc1bd098fe6f46cf09bc65 Mon Sep 17 00:00:00 2001 From: William Desportes Date: Tue, 8 Oct 2024 00:03:03 +0200 Subject: [PATCH] Validate IP addresses before insert --- snow-scanner/Cargo.toml | 6 ++ snow-scanner/src/event_bus.rs | 14 ++- snow-scanner/src/main.rs | 6 +- snow-scanner/src/worker/detection.rs | 10 +++ snow-scanner/src/worker/ip_addr.rs | 126 +++++++++++++++++++++++++++ snow-scanner/src/worker/mod.rs | 1 + snow-scanner/src/worker/worker.rs | 1 + 7 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 snow-scanner/src/worker/ip_addr.rs diff --git a/snow-scanner/Cargo.toml b/snow-scanner/Cargo.toml index fc3611e..b3a750f 100644 --- a/snow-scanner/Cargo.toml +++ b/snow-scanner/Cargo.toml @@ -37,6 +37,12 @@ members = [ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] + +# Enable unstable features, requires nightly +# Currently only used to enable rusts official ip support +unstable = [] + [dependencies] rocket = { git = "https://github.com/rwf2/Rocket/", rev = "3bf9ef02d6e803fe9f753777f5a829dda6d2453d"} rocket_ws = { git = "https://github.com/rwf2/Rocket/", rev = "3bf9ef02d6e803fe9f753777f5a829dda6d2453d"} diff --git a/snow-scanner/src/event_bus.rs b/snow-scanner/src/event_bus.rs index d67e15d..3f06474 100644 --- a/snow-scanner/src/event_bus.rs +++ b/snow-scanner/src/event_bus.rs @@ -1,6 +1,9 @@ use std::{net::IpAddr, str::FromStr}; -use crate::{worker::detection::detect_scanner_from_name, DbConnection, SnowDb}; +use crate::{ + worker::detection::{detect_scanner_from_name, validate_ip}, + DbConnection, SnowDb, +}; use hickory_resolver::Name; use rocket::futures::channel::mpsc as rocket_mpsc; use rocket::futures::StreamExt; @@ -49,12 +52,15 @@ impl EventBus { } match event { EventBusWriterEvent::ScannerFoundResponse { name, address } => { + let ip: IpAddr = address.into(); + if !validate_ip(ip) { + error!("Invalid IP address: {ip}"); + return; + } let name = Name::from_str(name.as_str()).unwrap(); match detect_scanner_from_name(&name) { Ok(Some(scanner_type)) => { - match Scanner::find_or_new(address.into(), scanner_type, Some(name), db) - .await - { + match Scanner::find_or_new(ip, scanner_type, Some(name), db).await { Ok(scanner) => { let _ = scanner.save(db).await; } diff --git a/snow-scanner/src/main.rs b/snow-scanner/src/main.rs index 9539d0f..b633eb2 100644 --- a/snow-scanner/src/main.rs +++ b/snow-scanner/src/main.rs @@ -37,7 +37,7 @@ use rocket_db_pools::Database; use rocket_ws::WebSocket; use server::Server; -use worker::detection::{detect_scanner, get_dns_client, Scanners}; +use worker::detection::{detect_scanner, get_dns_client, validate_ip, Scanners}; use std::{ env, fmt, @@ -209,6 +209,10 @@ async fn handle_ip(mut conn: DbConn, ip: String) -> Result { + if !validate_ip(query_address) { + error!("Invalid IP address: {ip}"); + return Err(None); + } match Scanner::find_or_new(query_address, scanner_type, result.result, &mut conn).await { Ok(scanner) => Ok(scanner), diff --git a/snow-scanner/src/worker/detection.rs b/snow-scanner/src/worker/detection.rs index 8570a20..1cb8ddb 100644 --- a/snow-scanner/src/worker/detection.rs +++ b/snow-scanner/src/worker/detection.rs @@ -8,6 +8,8 @@ use dns_ptr_resolver::ResolvedResult; use hickory_resolver::config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}; use hickory_resolver::{Name, Resolver}; +use crate::worker::ip_addr::is_global_hardcoded; + #[derive(Debug, Clone, Copy, FromSqlRow)] pub enum Scanners { Stretchoid, @@ -33,6 +35,14 @@ pub fn get_dns_client() -> Resolver { Resolver::new(config, options).unwrap() } +pub fn validate_ip(ip: IpAddr) -> bool { + // unspecified => 0.0.0.0 + if ip.is_loopback() || ip.is_multicast() || ip.is_unspecified() { + return false; + } + return is_global_hardcoded(ip); +} + pub fn detect_scanner(ptr_result: &ResolvedResult) -> Result, ()> { match &ptr_result.result { Some(name) => detect_scanner_from_name(&name), diff --git a/snow-scanner/src/worker/ip_addr.rs b/snow-scanner/src/worker/ip_addr.rs new file mode 100644 index 0000000..b5b67d2 --- /dev/null +++ b/snow-scanner/src/worker/ip_addr.rs @@ -0,0 +1,126 @@ +// +// Port of the official Rust implementation +// Source: https://github.com/dani-garcia/vaultwarden/blob/1.32.1/src/util.rs +// + +/// TODO: This is extracted from IpAddr::is_global, which is unstable: +/// https://doc.rust-lang.org/nightly/std/net/enum.IpAddr.html#method.is_global +/// Remove once https://github.com/rust-lang/rust/issues/27709 is merged +#[allow(clippy::nonminimal_bool)] +#[cfg(any(not(feature = "unstable"), test))] +pub fn is_global_hardcoded(ip: std::net::IpAddr) -> bool { + match ip { + std::net::IpAddr::V4(ip) => { + !(ip.octets()[0] == 0 // "This network" + || ip.is_private() + || (ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000 == 0b0100_0000)) //ip.is_shared() + || ip.is_loopback() + || ip.is_link_local() + // addresses reserved for future protocols (`192.0.0.0/24`) + ||(ip.octets()[0] == 192 && ip.octets()[1] == 0 && ip.octets()[2] == 0) + || ip.is_documentation() + || (ip.octets()[0] == 198 && (ip.octets()[1] & 0xfe) == 18) // ip.is_benchmarking() + || (ip.octets()[0] & 240 == 240 && !ip.is_broadcast()) //ip.is_reserved() + || ip.is_broadcast()) + } + std::net::IpAddr::V6(ip) => { + !(ip.is_unspecified() + || ip.is_loopback() + // IPv4-mapped Address (`::ffff:0:0/96`) + || matches!(ip.segments(), [0, 0, 0, 0, 0, 0xffff, _, _]) + // IPv4-IPv6 Translat. (`64:ff9b:1::/48`) + || matches!(ip.segments(), [0x64, 0xff9b, 1, _, _, _, _, _]) + // Discard-Only Address Block (`100::/64`) + || matches!(ip.segments(), [0x100, 0, 0, 0, _, _, _, _]) + // IETF Protocol Assignments (`2001::/23`) + || (matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200) + && !( + // Port Control Protocol Anycast (`2001:1::1`) + u128::from_be_bytes(ip.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001 + // Traversal Using Relays around NAT Anycast (`2001:1::2`) + || u128::from_be_bytes(ip.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002 + // AMT (`2001:3::/32`) + || matches!(ip.segments(), [0x2001, 3, _, _, _, _, _, _]) + // AS112-v6 (`2001:4:112::/48`) + || matches!(ip.segments(), [0x2001, 4, 0x112, _, _, _, _, _]) + // ORCHIDv2 (`2001:20::/28`) + || matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if (0x20..=0x2F).contains(&b)) + )) + || ((ip.segments()[0] == 0x2001) && (ip.segments()[1] == 0xdb8)) // ip.is_documentation() + || ((ip.segments()[0] & 0xfe00) == 0xfc00) //ip.is_unique_local() + || ((ip.segments()[0] & 0xffc0) == 0xfe80)) //ip.is_unicast_link_local() + } + } +} + +#[cfg(not(feature = "unstable"))] +pub use is_global_hardcoded as is_global; + +#[cfg(feature = "unstable")] +#[inline(always)] +pub fn is_global(ip: std::net::IpAddr) -> bool { + ip.is_global() +} + +/// These are some tests to check that the implementations match +/// The IPv4 can be all checked in 30 seconds or so and they are correct as of nightly 2023-07-17 +/// The IPV6 can't be checked in a reasonable time, so we check over a hundred billion random ones, so far correct +/// Note that the is_global implementation is subject to change as new IP RFCs are created +/// +/// To run while showing progress output: +/// cargo +nightly test --release --features sqlite,unstable -- --nocapture --ignored +#[cfg(test)] +#[cfg(feature = "unstable")] +mod tests { + use super::*; + use std::net::IpAddr; + + #[test] + #[ignore] + fn test_ipv4_global() { + for a in 0..u8::MAX { + println!("Iter: {}/255", a); + for b in 0..u8::MAX { + for c in 0..u8::MAX { + for d in 0..u8::MAX { + let ip = IpAddr::V4(std::net::Ipv4Addr::new(a, b, c, d)); + assert_eq!( + ip.is_global(), + is_global_hardcoded(ip), + "IP mismatch: {}", + ip + ) + } + } + } + } + } + + #[test] + #[ignore] + fn test_ipv6_global() { + use rand::Rng; + + std::thread::scope(|s| { + for t in 0..16 { + let handle = s.spawn(move || { + let mut v = [0u8; 16]; + let mut rng = rand::thread_rng(); + + for i in 0..20 { + println!("Thread {t} Iter: {i}/50"); + for _ in 0..500_000_000 { + rng.fill(&mut v); + let ip = IpAddr::V6(std::net::Ipv6Addr::from(v)); + assert_eq!( + ip.is_global(), + is_global_hardcoded(ip), + "IP mismatch: {ip}" + ); + } + } + }); + } + }); + } +} diff --git a/snow-scanner/src/worker/mod.rs b/snow-scanner/src/worker/mod.rs index 5ff9510..addc8a7 100644 --- a/snow-scanner/src/worker/mod.rs +++ b/snow-scanner/src/worker/mod.rs @@ -1,2 +1,3 @@ pub mod detection; +pub mod ip_addr; pub mod modules; diff --git a/snow-scanner/src/worker/worker.rs b/snow-scanner/src/worker/worker.rs index 34ea9b5..28f950a 100644 --- a/snow-scanner/src/worker/worker.rs +++ b/snow-scanner/src/worker/worker.rs @@ -8,6 +8,7 @@ use tungstenite::stream::MaybeTlsStream; use tungstenite::{connect, Error, Message, WebSocket}; pub mod detection; +pub mod ip_addr; pub mod modules; use crate::detection::get_dns_client;