mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
fix(client): reject all obfuscated IPv4 host forms at parse time
Replace the narrow decimal/hex obfuscation check with a general comparison: if the parsed host is an IPv4 literal and the raw input host differs from the canonical dotted-quad form, the user supplied an obfuscated variant (octal, short-form, mixed radix, leading zeros, decimal integer, hex integer) that url::Url has already normalized to 127.0.0.1. All such variants are rejected. Test now covers decimal, hex, octal, two-/three-part short, mixed hex/ decimal, and leading-zero octets. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1214bebd14
commit
125a73aae3
2 changed files with 21 additions and 14 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -146,13 +146,16 @@ fn canonical_http_url(value: &str) -> Result<String> {
|
|||
_ => 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::<std::net::Ipv4Addr>().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<PathBuf> {
|
||||
if !path.is_absolute() {
|
||||
bail!("server target must be an http(s) URL or absolute Unix socket path");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue