use std::net::IpAddr;
use std::str::FromStr;
use ipnetwork::IpNetwork;
use tracing::warn;
pub struct HostHeaders<'a> {
pub host: Option<&'a str>,
pub forwarded: Option<&'a str>,
pub x_forwarded_host: Option<&'a str>,
}
pub fn resolve_request_host<'a>(
headers: &'a HostHeaders<'_>,
peer_ip: Option<IpAddr>,
trusted_cidrs: &[IpNetwork],
) -> Option<&'a str> {
let peer_is_trusted = peer_is_trusted(peer_ip, trusted_cidrs);
if peer_is_trusted {
if let Some(fwd) = headers.forwarded
&& let Some(host) = parse_forwarded_host(fwd)
{
return Some(host);
}
if let Some(xfh) = headers.x_forwarded_host
&& let Some(first) = first_xff_value(xfh)
{
return Some(first);
}
} else if headers.forwarded.is_some() || headers.x_forwarded_host.is_some() {
warn!(
peer = ?peer_ip,
"request from untrusted peer claims Forwarded / X-Forwarded-Host; ignoring"
);
}
headers.host
}
fn peer_is_trusted(peer_ip: Option<IpAddr>, trusted_cidrs: &[IpNetwork]) -> bool {
let Some(ip) = peer_ip else {
return false;
};
trusted_cidrs.iter().any(|cidr| cidr.contains(ip))
}
fn first_xff_value(header: &str) -> Option<&str> {
let first = header.split(',').next()?.trim();
if first.is_empty() { None } else { Some(first) }
}
pub fn parse_forwarded_host(header: &str) -> Option<&str> {
let first_group = header.split(',').next()?;
for raw_pair in first_group.split(';') {
let pair = raw_pair.trim();
let (name, value) = pair.split_once('=')?;
if !name.trim().eq_ignore_ascii_case("host") {
continue;
}
let value = value.trim();
let stripped = value
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(value);
if stripped.is_empty() {
return None;
}
return Some(stripped);
}
None
}
pub fn parse_trusted_cidrs(input: &[String]) -> (Vec<IpNetwork>, Vec<String>) {
let mut ok = Vec::with_capacity(input.len());
let mut bad = Vec::new();
for s in input {
match IpNetwork::from_str(s) {
Ok(net) => ok.push(net),
Err(_) => bad.push(s.clone()),
}
}
(ok, bad)
}
#[cfg(test)]
mod tests {
use super::*;
fn cidr(s: &str) -> IpNetwork {
IpNetwork::from_str(s).expect("valid cidr")
}
fn ip(s: &str) -> IpAddr {
IpAddr::from_str(s).expect("valid ip")
}
fn h(
host: Option<&'static str>,
fwd: Option<&'static str>,
xfh: Option<&'static str>,
) -> HostHeaders<'static> {
HostHeaders {
host,
forwarded: fwd,
x_forwarded_host: xfh,
}
}
#[test]
fn peer_inside_cidr_is_trusted() {
let cidrs = vec![cidr("10.0.0.0/8")];
assert!(peer_is_trusted(Some(ip("10.1.2.3")), &cidrs));
}
#[test]
fn peer_outside_cidr_is_untrusted() {
let cidrs = vec![cidr("10.0.0.0/8")];
assert!(!peer_is_trusted(Some(ip("172.16.1.1")), &cidrs));
}
#[test]
fn empty_cidr_list_trusts_no_one() {
assert!(!peer_is_trusted(Some(ip("10.0.0.1")), &[]));
assert!(!peer_is_trusted(Some(ip("127.0.0.1")), &[]));
}
#[test]
fn missing_peer_ip_is_untrusted() {
assert!(!peer_is_trusted(None, &[cidr("0.0.0.0/0")]));
}
#[test]
fn untrusted_peer_uses_host_header() {
let headers = h(
Some("example.com"),
Some("host=evil.example"),
Some("evil.example"),
);
let cidrs = vec![cidr("10.0.0.0/8")];
let resolved = resolve_request_host(&headers, Some(ip("8.8.8.8")), &cidrs);
assert_eq!(resolved, Some("example.com"));
}
#[test]
fn untrusted_peer_xfh_spoof_has_no_effect() {
let headers = h(Some("real.example.com"), None, Some("victim.example"));
let resolved = resolve_request_host(&headers, Some(ip("8.8.8.8")), &[]);
assert_eq!(resolved, Some("real.example.com"));
}
#[test]
fn missing_host_yields_none() {
let headers = h(None, None, None);
let resolved = resolve_request_host(&headers, Some(ip("8.8.8.8")), &[]);
assert_eq!(resolved, None);
}
#[test]
fn trusted_peer_prefers_forwarded_over_xfh_over_host() {
let headers = h(
Some("fallback.example"),
Some("host=forwarded.example"),
Some("xfh.example"),
);
let cidrs = vec![cidr("10.0.0.0/8")];
let resolved = resolve_request_host(&headers, Some(ip("10.1.2.3")), &cidrs);
assert_eq!(resolved, Some("forwarded.example"));
}
#[test]
fn trusted_peer_uses_xfh_when_forwarded_absent() {
let headers = h(Some("fallback.example"), None, Some("xfh.example"));
let cidrs = vec![cidr("10.0.0.0/8")];
let resolved = resolve_request_host(&headers, Some(ip("10.1.2.3")), &cidrs);
assert_eq!(resolved, Some("xfh.example"));
}
#[test]
fn trusted_peer_xfh_first_value_wins() {
let headers = h(
None,
None,
Some("client.example, proxy1.example, proxy2.example"),
);
let cidrs = vec![cidr("10.0.0.0/8")];
let resolved = resolve_request_host(&headers, Some(ip("10.0.0.5")), &cidrs);
assert_eq!(resolved, Some("client.example"));
}
#[test]
fn trusted_peer_falls_back_to_host_when_forwarded_headers_empty() {
let headers = h(Some("only.example"), None, None);
let cidrs = vec![cidr("10.0.0.0/8")];
let resolved = resolve_request_host(&headers, Some(ip("10.1.2.3")), &cidrs);
assert_eq!(resolved, Some("only.example"));
}
#[test]
fn forwarded_parses_unquoted_host() {
assert_eq!(
parse_forwarded_host("host=example.com"),
Some("example.com")
);
}
#[test]
fn forwarded_parses_quoted_host() {
assert_eq!(
parse_forwarded_host(r#"host="example.com""#),
Some("example.com")
);
}
#[test]
fn forwarded_parses_with_other_params() {
assert_eq!(
parse_forwarded_host("for=192.0.2.60;host=example.com;proto=https"),
Some("example.com")
);
}
#[test]
fn forwarded_is_case_insensitive_for_param_name() {
assert_eq!(
parse_forwarded_host("HOST=example.com"),
Some("example.com")
);
assert_eq!(
parse_forwarded_host("Host=example.com"),
Some("example.com")
);
}
#[test]
fn forwarded_multiple_groups_honours_first() {
assert_eq!(
parse_forwarded_host("host=first.example, host=second.example"),
Some("first.example")
);
}
#[test]
fn forwarded_missing_host_param() {
assert_eq!(parse_forwarded_host("for=192.0.2.60;proto=https"), None);
}
#[test]
fn forwarded_empty_value_returns_none() {
assert_eq!(parse_forwarded_host("host="), None);
assert_eq!(parse_forwarded_host(r#"host="""#), None);
}
#[test]
fn forwarded_handles_ipv6_bracketed_host() {
assert_eq!(
parse_forwarded_host(r#"host="[2001:db8::1]:8080""#),
Some("[2001:db8::1]:8080")
);
}
#[test]
fn parse_trusted_cidrs_separates_good_and_bad() {
let inputs = vec![
"10.0.0.0/8".to_string(),
"garbage".to_string(),
"2001:db8::/32".to_string(),
"192.168.1.300/24".to_string(), ];
let (ok, bad) = parse_trusted_cidrs(&inputs);
assert_eq!(ok.len(), 2);
assert_eq!(bad, vec!["garbage", "192.168.1.300/24"]);
}
#[test]
fn realistic_aws_alb_scenario() {
let cidrs = vec![cidr("10.0.0.0/16")];
let alb_ip = Some(ip("10.0.5.42"));
let headers = h(Some("internal.example"), None, Some("tenant-a.example.com"));
assert_eq!(
resolve_request_host(&headers, alb_ip, &cidrs),
Some("tenant-a.example.com")
);
}
#[test]
fn realistic_direct_request_no_proxy() {
let headers = h(Some("example.com"), None, None);
let resolved = resolve_request_host(&headers, Some(ip("8.8.8.8")), &[]);
assert_eq!(resolved, Some("example.com"));
}
}