From 30e25b4a4171589f71129a17a04d063c516b2e2e Mon Sep 17 00:00:00 2001 From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 Date: Tue, 30 Jun 2026 14:47:51 -0400 Subject: [PATCH] fix(relay): gate KIND_DREAM_DUE on matching #p to prevent info leak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KIND_DREAM_DUE (24300) was absent from P_GATED_KINDS, so p_gated_filters_authorized() never enforced #p-must-match-self for it. Any authenticated relay user could subscribe with kinds:[24300], #p:[victim_pubkey] and receive another agent's dream-due signals, leaking "agent X is over memory budget and idle" to anyone who knows an agent pubkey. The kind's own doc already states "single-delivery to the authenticated agent"; P_GATED_KINDS is the registry that makes the relay enforce it. Fix: add KIND_DREAM_DUE to P_GATED_KINDS. Ephemeral kinds are included in this list for filter-layer enforcement only and are never stored, so no schema/migration/tsvector change is needed. The ACP harness's own subscription (DREAM_SIGNAL_SUB_ID, #p=[self]) continues to be accepted — the self case is exactly what P_GATED_KINDS allows through. Test: dream_due_subscription_requires_matching_p_tag verifies: - no #p → rejected - #p:[other] → rejected - #p:[self] → accepted Confirmed: test fails without the registry add and passes with it. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- crates/buzz-core/src/kind.rs | 1 + crates/buzz-relay/src/handlers/req.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/crates/buzz-core/src/kind.rs b/crates/buzz-core/src/kind.rs index aabb9d07d..50113125f 100644 --- a/crates/buzz-core/src/kind.rs +++ b/crates/buzz-core/src/kind.rs @@ -131,6 +131,7 @@ pub const P_GATED_KINDS: &[u32] = &[ KIND_MEMBER_REMOVED_NOTIFICATION, KIND_GIFT_WRAP, KIND_DM_VISIBILITY, + KIND_DREAM_DUE, ]; /// NIP-AP: Agent Persona (parameterized replaceable, owner-authored). diff --git a/crates/buzz-relay/src/handlers/req.rs b/crates/buzz-relay/src/handlers/req.rs index 6667948eb..02c6829ab 100644 --- a/crates/buzz-relay/src/handlers/req.rs +++ b/crates/buzz-relay/src/handlers/req.rs @@ -1296,6 +1296,33 @@ mod tests { assert!(p_gated_filters_authorized(&[matching_p], authed)); } + #[test] + fn dream_due_subscription_requires_matching_p_tag() { + // KIND_DREAM_DUE (24300) is a #p-gated ephemeral kind: "single-delivery + // to the authenticated agent". A filter targeting another agent's pubkey + // must be rejected; a filter targeting self must be accepted. + let p_tag = SingleLetterTag::lowercase(Alphabet::P); + let authed = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let other = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + + // No #p at all → rejected (could subscribe to all agents' dream signals). + let no_p = Filter::new() + .kind(nostr::Kind::Custom(buzz_core::kind::KIND_DREAM_DUE as u16)); + assert!(!p_gated_filters_authorized(&[no_p], authed)); + + // #p targeting another agent → rejected (info leak: "agent X is over budget + idle"). + let wrong_p = Filter::new() + .kind(nostr::Kind::Custom(buzz_core::kind::KIND_DREAM_DUE as u16)) + .custom_tags(p_tag, [other]); + assert!(!p_gated_filters_authorized(&[wrong_p], authed)); + + // #p targeting self → accepted (this is the ACP harness's own subscription). + let self_p = Filter::new() + .kind(nostr::Kind::Custom(buzz_core::kind::KIND_DREAM_DUE as u16)) + .custom_tags(p_tag, [authed]); + assert!(p_gated_filters_authorized(&[self_p], authed)); + } + #[test] fn d_tag_pushdown_only_for_nip33_kinds() { let d_tag = SingleLetterTag::lowercase(Alphabet::D);