Skip to content

Commit f86f3bc

Browse files
laulpoganclaude
andauthored
test(relay): lock the handle_intro security filter against regression (#334 follow-up) (#335)
#334 fixed a HIGH boolean bug in the unauthenticated /v1/handle/intro filter (the `&&`-chain accepted a kind=1100 event of ANY type, and a wrong-kind event whose type happened to match) but added no test, so the exact bypass could silently come back. Lock it: - Extract the accept rule as a pure `intro_event_allowed(kind, type_str)` — `kind == 1100 && (type_str == "pair_drop" || type_str == "agent_card")` — and call it from the handler (no behavior change; just testable). - Regression test asserts the full accept/reject matrix, with the two #334 bypass classes called out explicitly: (a) kind=1100 + any other type rejected, (b) wrong-kind + matching type rejected. Pure refactor + test only; the live filter logic is byte-equivalent to #334's fix. 601 lib tests green, clippy clean. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0040dd2 commit f86f3bc

1 file changed

Lines changed: 33 additions & 1 deletion

File tree

src/relay_server.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1863,7 +1863,7 @@ async fn handle_intro(
18631863
// to the standard /v1/events/:slot_id with bearer auth.
18641864
let kind = req.event.get("kind").and_then(Value::as_u64).unwrap_or(0);
18651865
let type_str = req.event.get("type").and_then(Value::as_str).unwrap_or("");
1866-
if kind != 1100 || (type_str != "pair_drop" && type_str != "agent_card") {
1866+
if !intro_event_allowed(kind, type_str) {
18671867
return (
18681868
StatusCode::BAD_REQUEST,
18691869
Json(json!({
@@ -2295,6 +2295,17 @@ fn is_valid_slot_id(s: &str) -> bool {
22952295
.all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
22962296
}
22972297

2298+
/// The unauthenticated `/v1/handle/intro` endpoint accepts an event ONLY when it
2299+
/// is a `kind=1100` pair-intro of type `pair_drop` or `agent_card`; everything
2300+
/// else must route through the bearer-authed `/v1/events/:slot_id`. Extracted as
2301+
/// a pure predicate so the accept/reject matrix is locked by a unit test — #334
2302+
/// fixed a boolean bug here (`&&`-chain accepted a kind=1100 event of ANY type,
2303+
/// and a wrong-kind event whose type happened to match), and this guards the
2304+
/// security property against regressing.
2305+
fn intro_event_allowed(kind: u64, type_str: &str) -> bool {
2306+
kind == 1100 && (type_str == "pair_drop" || type_str == "agent_card")
2307+
}
2308+
22982309
/// A claimant-advertised `relay_url` is acceptable for the public directory iff
22992310
/// it is `http://` or `https://`, has a non-empty host authority, and carries NO
23002311
/// userinfo (`user@host` — the `handle@relay` bug) and no embedded
@@ -2646,4 +2657,25 @@ mod tests {
26462657
"0123456789abcdef\0\x31\x32\x33456789abcdef"
26472658
));
26482659
}
2660+
2661+
#[test]
2662+
fn intro_filter_accepts_only_kind_1100_pair_drop_or_agent_card() {
2663+
// The two legitimate intros: kind=1100 with pair_drop or agent_card.
2664+
assert!(intro_event_allowed(1100, "pair_drop"));
2665+
assert!(intro_event_allowed(1100, "agent_card"));
2666+
2667+
// #334 HIGH regression guards — the pre-fix `&&`-chain wrongly accepted
2668+
// BOTH of these:
2669+
// (a) a kind=1100 event of ANY other type must be rejected,
2670+
assert!(!intro_event_allowed(1100, "decision"));
2671+
assert!(!intro_event_allowed(1100, ""));
2672+
assert!(!intro_event_allowed(1100, "trust_revoke_key"));
2673+
// (b) a wrong-kind event whose type happens to match must be rejected.
2674+
assert!(!intro_event_allowed(1, "pair_drop"));
2675+
assert!(!intro_event_allowed(1101, "agent_card"));
2676+
assert!(!intro_event_allowed(0, "pair_drop"));
2677+
2678+
// Neither matching → rejected (the only case the old logic got right).
2679+
assert!(!intro_event_allowed(1, "decision"));
2680+
}
26492681
}

0 commit comments

Comments
 (0)