From 08f6daed39d36493f980e10e9a6e2f9f6b35cf63 Mon Sep 17 00:00:00 2001 From: Duncan Date: Mon, 10 Aug 2026 22:26:46 -0400 Subject: [PATCH] fix(relay): reject any unparseable BUZZ_ADMIN_HOST authority at config parse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bare-IPv6 bracket guard names the honest `::1` shape but skips unclosed-bracket typos like `[::1` and `[::1:3000` — they start with `[`, pass the guard, then interpolate into an unparseable `http://[::1` NIP-11 advertisement and NIP-98 u-tag URL. Same defect class as the bare-IPv6 case, just a typo shape. Add a catch-all after the bracket guard: url::Url::parse("http://{host}") must succeed, else reject with an error naming the host. This is a validity gate only — the host is still stored verbatim, not normalized. It kills every malformed authority in one guard, including shapes not enumerated. url is already a buzz-relay dep. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- crates/buzz-relay/src/config.rs | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/buzz-relay/src/config.rs b/crates/buzz-relay/src/config.rs index 6a31b4bd09..1c8d3fd284 100644 --- a/crates/buzz-relay/src/config.rs +++ b/crates/buzz-relay/src/config.rs @@ -1082,6 +1082,22 @@ impl Config { ))); } + // Catch-all authority gate: every accepted host is interpolated + // verbatim into the NIP-11 advertisement and NIP-98 `u`-tag URLs, + // so it must form a parseable authority. The bracket guard above + // names the honest bare-IPv6 shape; this rejects any remaining + // malformed authority (e.g. the unclosed-bracket typos `[::1` + // and `[::1:3000`) that would otherwise produce an unparseable + // `http://{host}`. This is a validity gate only — the host is + // still stored verbatim, not normalized. + if url::Url::parse(&format!("http://{host}")).is_err() { + return Err(ConfigError::InvalidValue(format!( + "BUZZ_ADMIN_HOST={host} is not a valid URL authority; \ + it must be a host with an optional port, e.g. \ + relay.example.com:8443 or [::1]:3000" + ))); + } + // Parse BUZZ_ADMIN_AUTH. Accepted values: "token" (default when // unset), "disabled", "nip98". Any other non-empty value is a // startup error (typo-proofing). @@ -1446,6 +1462,29 @@ mod tests { } } + #[test] + fn admin_host_malformed_authority_fails_closed() { + // Shapes that slip the bare-IPv6 bracket guard (they start with `[`) + // but still cannot form a valid URL authority — the unclosed-bracket + // typos. Without the catch-all parse gate these would produce an + // unparseable `http://[::1` advertisement and NIP-98 `u`-tag URL. + let _guard = ENV_MUTEX.lock().unwrap(); + for host in ["[::1", "[::1:3000", "[not-closed"] { + let result = config_with_admin_env(&[ + ("BUZZ_ADMIN_HOST", Some(host)), + ("BUZZ_ADMIN_TOKEN", Some(VALID_ADMIN_TOKEN)), + ]); + assert!( + matches!( + result, + Err(ConfigError::InvalidValue(ref message)) + if message.contains("BUZZ_ADMIN_HOST") && message.contains("valid URL authority") + ), + "malformed authority {host:?} must be rejected: {result:?}" + ); + } + } + #[test] fn admin_host_bracketed_ipv6_literal_is_accepted() { let _guard = ENV_MUTEX.lock().unwrap();