test(security): add HTTP hostname canonicalization edge-case tests (#522)
* test(security): add HTTP hostname canonicalization edge-case tests Document that Rust's IpAddr::parse() rejects non-standard IP notations (octal, hex, decimal integer, zero-padded) which provides defense-in-depth against SSRF bypass attempts. Tests only — no production code changes. Closes #515 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: apply rustfmt to providers/mod.rs Fix pre-existing formatting issue from main. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e3f00e82b9
commit
55b3c2c00c
1 changed files with 50 additions and 0 deletions
|
|
@ -749,4 +749,54 @@ mod tests {
|
||||||
let _ = HttpRequestTool::redact_headers_for_display(&headers);
|
let _ = HttpRequestTool::redact_headers_for_display(&headers);
|
||||||
assert_eq!(headers[0].1, "Bearer real-token");
|
assert_eq!(headers[0].1, "Bearer real-token");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── SSRF: alternate IP notation bypass defense-in-depth ─────────
|
||||||
|
//
|
||||||
|
// Rust's IpAddr::parse() rejects non-standard notations (octal, hex,
|
||||||
|
// decimal integer, zero-padded). These tests document that property
|
||||||
|
// so regressions are caught if the parsing strategy ever changes.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ssrf_octal_loopback_not_parsed_as_ip() {
|
||||||
|
// 0177.0.0.1 is octal for 127.0.0.1 in some languages, but
|
||||||
|
// Rust's IpAddr rejects it — it falls through as a hostname.
|
||||||
|
assert!(!is_private_or_local_host("0177.0.0.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ssrf_hex_loopback_not_parsed_as_ip() {
|
||||||
|
// 0x7f000001 is hex for 127.0.0.1 in some languages.
|
||||||
|
assert!(!is_private_or_local_host("0x7f000001"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ssrf_decimal_loopback_not_parsed_as_ip() {
|
||||||
|
// 2130706433 is decimal for 127.0.0.1 in some languages.
|
||||||
|
assert!(!is_private_or_local_host("2130706433"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ssrf_zero_padded_loopback_not_parsed_as_ip() {
|
||||||
|
// 127.000.000.001 uses zero-padded octets.
|
||||||
|
assert!(!is_private_or_local_host("127.000.000.001"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ssrf_alternate_notations_rejected_by_validate_url() {
|
||||||
|
// Even if is_private_or_local_host doesn't flag these, they
|
||||||
|
// fail the allowlist because they're treated as hostnames.
|
||||||
|
let tool = test_tool(vec!["example.com"]);
|
||||||
|
for notation in [
|
||||||
|
"http://0177.0.0.1",
|
||||||
|
"http://0x7f000001",
|
||||||
|
"http://2130706433",
|
||||||
|
"http://127.000.000.001",
|
||||||
|
] {
|
||||||
|
let err = tool.validate_url(notation).unwrap_err().to_string();
|
||||||
|
assert!(
|
||||||
|
err.contains("allowed_domains"),
|
||||||
|
"Expected allowlist rejection for {notation}, got: {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue