fix(relay): reject any unparseable BUZZ_ADMIN_HOST authority at config parse

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 <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
Duncan
2026-08-10 22:26:46 -04:00
parent 1f37a26ae7
commit 08f6daed39

View File

@@ -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();