Skip to content

Commit c024912

Browse files
committed
fix(security): harden is_loopback_bind via IpAddr parse (review nit)
Parse the bind host as an IpAddr and use is_loopback() instead of a `starts_with("127.")` shortcut, which would have treated a hostname like `127.example.com` as loopback (fail-open). Also accept the bracketed IPv6 literal `[::1]`. Anything unparseable is treated as non-loopback (fails safe, requires a token). Addresses the LOW review finding on #40.
1 parent 3edcfcf commit c024912

1 file changed

Lines changed: 22 additions & 4 deletions

File tree

src/channels/api.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -370,11 +370,24 @@ impl ApiChannel {
370370
}
371371

372372
/// Loopback / local-only binds that are safe to serve without authentication.
373+
///
374+
/// Parses the host as an IP and uses `is_loopback()` (covers all of `127.0.0.0/8` and `::1`,
375+
/// including the bracketed `[::1]` form), so a non-loopback bind (`0.0.0.0`, `::`, a public IP) —
376+
/// or anything unparseable, including a hostname like `127.example.com` — is treated as NOT
377+
/// loopback and therefore requires a token. Fails safe (unknown ⇒ not loopback).
373378
fn is_loopback_bind(addr: &str) -> bool {
374-
match addr.trim() {
375-
"127.0.0.1" | "::1" | "localhost" => true,
376-
host => host.starts_with("127."),
379+
let host = addr.trim();
380+
if host.eq_ignore_ascii_case("localhost") {
381+
return true;
377382
}
383+
// Accept the bracketed IPv6 literal `[::1]` as well as bare `::1`.
384+
let host = host
385+
.strip_prefix('[')
386+
.and_then(|h| h.strip_suffix(']'))
387+
.unwrap_or(host);
388+
host.parse::<std::net::IpAddr>()
389+
.map(|ip| ip.is_loopback())
390+
.unwrap_or(false)
378391
}
379392

380393
/// Refuse to expose the control plane to a non-loopback address without a token. The `/v1`
@@ -2689,10 +2702,15 @@ bind_address = "127.0.0.1"
26892702
fn loopback_binds_recognized() {
26902703
assert!(is_loopback_bind("127.0.0.1"));
26912704
assert!(is_loopback_bind("::1"));
2705+
assert!(is_loopback_bind("[::1]")); // bracketed IPv6 literal
26922706
assert!(is_loopback_bind("localhost"));
2693-
assert!(is_loopback_bind("127.0.0.5"));
2707+
assert!(is_loopback_bind("127.0.0.5")); // all of 127.0.0.0/8
26942708
assert!(!is_loopback_bind("0.0.0.0"));
2709+
assert!(!is_loopback_bind("::")); // unspecified IPv6 is NOT loopback
26952710
assert!(!is_loopback_bind("192.168.1.10"));
2711+
// A hostname that merely starts with "127." must NOT be treated as loopback
2712+
// (previously fail-open via a `starts_with("127.")` shortcut).
2713+
assert!(!is_loopback_bind("127.example.com"));
26962714
}
26972715

26982716
#[test]

0 commit comments

Comments
 (0)