diff --git a/lib/crates/fabro-client/src/loopback.rs b/lib/crates/fabro-client/src/loopback.rs index bda6f0b25..cea27093f 100644 --- a/lib/crates/fabro-client/src/loopback.rs +++ b/lib/crates/fabro-client/src/loopback.rs @@ -176,7 +176,17 @@ mod tests { #[test] fn rejects_obfuscated_ipv4_literals_at_parse_time() { - for api_url in ["http://2130706433", "http://0x7f000001"] { + let cases = [ + "http://2130706433", // decimal integer + "http://0x7f000001", // hex integer + "http://0177.0.0.1", // octal dotted + "http://127.1", // two-part short + "http://127.0.1", // three-part short + "http://0x7f.0.0.1", // mixed hex/decimal + "http://127.00.0.1", // leading-zero octet + "http://127.0.0.001", // leading-zero octet + ]; + for api_url in cases { assert!( ServerTarget::http_url(api_url).is_err(), "{api_url} should not parse as a server target" diff --git a/lib/crates/fabro-client/src/target.rs b/lib/crates/fabro-client/src/target.rs index 4ad663ac5..b590bcb0a 100644 --- a/lib/crates/fabro-client/src/target.rs +++ b/lib/crates/fabro-client/src/target.rs @@ -146,13 +146,16 @@ fn canonical_http_url(value: &str) -> Result { _ => bail!("server target must be an http(s) URL or absolute Unix socket path"), }; - if raw_url_host(normalized).is_some_and(is_obfuscated_ipv4_literal) { - bail!("server target must be an http(s) URL or absolute Unix socket path"); - } - let Some(host) = url.host_str() else { bail!("server target must be an http(s) URL or absolute Unix socket path"); }; + + if host.parse::().is_ok() + && raw_url_host(normalized).is_some_and(|raw| raw != host) + { + bail!("server target must be an http(s) URL or absolute Unix socket path"); + } + let host = host.to_ascii_lowercase(); let Some(port) = url.port_or_known_default() else { bail!("server target must be an http(s) URL or absolute Unix socket path"); @@ -171,8 +174,9 @@ fn trim_api_path_suffix(value: &str) -> &str { } /// Extract the host substring from `value` without going through -/// [`fabro_http::Url`]. `Url` normalizes decimal/hex IPv4 literals into dotted -/// form, which hides the original input from later inspection. +/// [`fabro_http::Url`]. `Url` normalizes IPv4 literals (decimal/hex/octal/short +/// form) into dotted quads, which hides the original input from later +/// inspection. fn raw_url_host(value: &str) -> Option<&str> { let (_, remainder) = value.split_once("://")?; let authority_end = remainder.find(['/', '?', '#']).unwrap_or(remainder.len()); @@ -189,13 +193,6 @@ fn raw_url_host(value: &str) -> Option<&str> { (!host.is_empty()).then_some(host) } -fn is_obfuscated_ipv4_literal(host: &str) -> bool { - if host.starts_with("0x") || host.starts_with("0X") { - return true; - } - !host.contains('.') && !host.is_empty() && host.bytes().all(|b| b.is_ascii_digit()) -} - fn lexical_normalize_absolute_path(path: &Path) -> Result { if !path.is_absolute() { bail!("server target must be an http(s) URL or absolute Unix socket path");