Skip to content

Commit 0d86c18

Browse files
committed
fix: add public host validation to prevent private host usage
1 parent 9dd2365 commit 0d86c18

File tree

2 files changed

+63
-0
lines changed

2 files changed

+63
-0
lines changed

crates/http-backend/src/lib.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod stats;
22

33
use std::fmt::Debug;
44
use std::future::Future;
5+
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
56
use std::pin::Pin;
67
use std::sync::Arc;
78
use std::task::{Context, Poll};
@@ -219,6 +220,13 @@ impl<C> Backend<C> {
219220
let original_host = original_host
220221
.or_else(|| request_host_header.clone())
221222
.unwrap_or_default();
223+
224+
anyhow::ensure!(
225+
is_public_host(&original_host),
226+
"private host not allowed: {}",
227+
original_host
228+
);
229+
222230
// filter headers
223231
let mut headers = req
224232
.headers
@@ -488,6 +496,54 @@ impl hyper_util::client::legacy::connect::Connection for Connection {
488496
}
489497
}
490498

499+
pub fn is_public_host(host: &str) -> bool {
500+
// Try to parse as IP address
501+
match host.parse::<IpAddr>() {
502+
Ok(ip) => !is_private_ip(&ip),
503+
Err(_) => true, // Not an IP address, assume it's a hostname
504+
}
505+
}
506+
507+
fn is_private_ip(ip: &IpAddr) -> bool {
508+
match ip {
509+
IpAddr::V4(ipv4) => is_private_ipv4(ipv4),
510+
IpAddr::V6(ipv6) => is_private_ipv6(ipv6),
511+
}
512+
}
513+
514+
/// Check if an IPv4 address is private
515+
fn is_private_ipv4(ip: &Ipv4Addr) -> bool {
516+
ip.octets()[0] == 0 // "This network"
517+
|| ip.is_private()
518+
|| ip.is_loopback()
519+
|| ip.is_link_local()
520+
|| (
521+
ip.octets()[0] == 192 && ip.octets()[1] == 0 && ip.octets()[2] == 0
522+
&& ip.octets()[3] != 9 && ip.octets()[3] != 10
523+
)
524+
|| ip.is_documentation()
525+
|| ip.is_broadcast()
526+
}
527+
528+
/// Check if an IPv6 address is private
529+
fn is_private_ipv6(ip: &Ipv6Addr) -> bool {
530+
ip.is_unspecified()
531+
|| ip.is_loopback()
532+
|| matches!(ip.segments(), [0, 0, 0, 0, 0, 0xffff, _, _])
533+
|| matches!(ip.segments(), [0x64, 0xff9b, 1, _, _, _, _, _])
534+
|| matches!(ip.segments(), [0x100, 0, 0, 0, _, _, _, _])
535+
|| (matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200)
536+
&& !(u128::from_be_bytes(ip.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001
537+
|| u128::from_be_bytes(ip.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002
538+
|| matches!(ip.segments(), [0x2001, 3, _, _, _, _, _, _])
539+
|| matches!(ip.segments(), [0x2001, 4, 0x112, _, _, _, _, _])
540+
|| matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if (0x20..=0x3F).contains(&b))))
541+
|| matches!(ip.segments(), [0x2002, _, _, _, _, _, _, _])
542+
|| matches!(ip.segments(), [0x5f00, ..])
543+
|| ip.is_unique_local()
544+
|| ip.is_unicast_link_local()
545+
}
546+
491547
#[cfg(test)]
492548
mod tests {
493549
use super::*;

crates/http-service/src/state.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use anyhow::Error;
22
use http::request::Parts;
33
use http::uri::Scheme;
44
use http::{header, HeaderMap, HeaderName, Uri};
5+
use http_backend::is_public_host;
56
use http_backend::Backend;
67
use runtime::store::HasStats;
78
use runtime::util::stats::StatsVisitor;
@@ -47,6 +48,12 @@ impl<C> BackendRequest for HttpState<C> {
4748
})
4849
.unwrap_or_default();
4950

51+
anyhow::ensure!(
52+
is_public_host(&original_host),
53+
"private host not allowed: {}",
54+
original_host
55+
);
56+
5057
static FILTER_HEADERS: [HeaderName; 6] = [
5158
header::HOST,
5259
header::CONTENT_LENGTH,

0 commit comments

Comments
 (0)