buzz_sdk::builders::build_message accepts mention pubkeys that are not valid hex public keys and signs them into p tags unchanged. Reported from a downstream consumer (a UniFFI mobile core); filing here rather than working around it locally, since the decision affects desktop and CLI consumers too.
Behavior
mention_tags (crates/buzz-sdk/src/builders.rs:192-205) lowercases each entry and pushes ["p", lower] with no hex/length check:
fn mention_tags(mentions: &[&str], tags: &mut Vec<Tag>) -> Result<(), SdkError> {
if mentions.len() > crate::mentions::MENTION_CAP {
return Err(SdkError::TooManyMentions);
}
let mut seen = std::collections::HashSet::new();
for &hex in mentions {
let lower = hex.to_ascii_lowercase();
if seen.insert(lower.clone()) {
tags.push(tag(&["p", &lower])?);
}
}
Ok(())
}
Tag::parse does not validate that a p value is a 32-byte hex key, and mentions::normalize_mention_pubkeys (crates/buzz-sdk/src/mentions.rs:228) only lowercases, dedupes, and drops the sender — so nothing on this path rejects a malformed key. Note the asymmetry: the NIP-27 path does validate, via PublicKey::from_bech32 at mentions.rs:378. Only explicitly-supplied pubkeys skip validation.
Reproduction
Against 93114c9c (crates/buzz-sdk), calling build_message with four invalid mentions and signing:
let garbage = ["not-a-pubkey", "ZZZZ", "", "../../etc/passwd"];
let builder = buzz_sdk::builders::build_message(
channel, "hello", None, &garbage, false, &[],
).expect("build_message accepted non-hex mentions");
let event = builder.sign_with_keys(&keys).expect("signed");
build_message returns Ok, the event signs, and the signed tags are:
[["p", "not-a-pubkey"], ["p", "zzzz"], ["p", ""], ["p", "../../etc/passwd"]]
An empty p value and a path-shaped p value both survive into a signed event.
Why it matters downstream
For SDK consumers whose mention list crosses a boundary from user-influenced data (an FFI surface, an MCP tool call, a CLI flag), the builder is the natural validation point: a caller reasonably assumes a function taking "mention pubkeys" rejects things that are not pubkeys. Instead the invalid tag is signed and published, and every reader has to defend against p tags that cannot be parsed as keys. It also silently burns mention-cap budget on entries that can never address anyone.
I have no evidence of a live exploit — the practical impact I can demonstrate is malformed signed events and wasted cap, not a relay-side vulnerability.
Suggested fix (upstream's call)
Validate each entry through nostr::PublicKey::from_hex in mention_tags and return a typed error (e.g. an SdkError::InvalidMentionPubkey { value }, alongside the existing TooManyMentions) rather than silently dropping — a silent drop would make a mention vanish with no feedback to the composer.
Two things worth deciding deliberately, which is why I am not sending a patch:
- Whether it belongs in
mention_tags or in normalize_mention_pubkeys. Putting it in normalize_* catches more callers but changes a currently-infallible signature.
- Whether rejecting is a breaking change for existing consumers. If any caller today passes non-key values through
p tags deliberately, validation would start failing their sends.
Happy to send a PR once you have a preference on those two points.
buzz_sdk::builders::build_messageaccepts mention pubkeys that are not valid hex public keys and signs them intoptags unchanged. Reported from a downstream consumer (a UniFFI mobile core); filing here rather than working around it locally, since the decision affects desktop and CLI consumers too.Behavior
mention_tags(crates/buzz-sdk/src/builders.rs:192-205) lowercases each entry and pushes["p", lower]with no hex/length check:Tag::parsedoes not validate that apvalue is a 32-byte hex key, andmentions::normalize_mention_pubkeys(crates/buzz-sdk/src/mentions.rs:228) only lowercases, dedupes, and drops the sender — so nothing on this path rejects a malformed key. Note the asymmetry: the NIP-27 path does validate, viaPublicKey::from_bech32atmentions.rs:378. Only explicitly-supplied pubkeys skip validation.Reproduction
Against
93114c9c(crates/buzz-sdk), callingbuild_messagewith four invalid mentions and signing:build_messagereturnsOk, the event signs, and the signed tags are:An empty
pvalue and a path-shapedpvalue both survive into a signed event.Why it matters downstream
For SDK consumers whose mention list crosses a boundary from user-influenced data (an FFI surface, an MCP tool call, a CLI flag), the builder is the natural validation point: a caller reasonably assumes a function taking "mention pubkeys" rejects things that are not pubkeys. Instead the invalid tag is signed and published, and every reader has to defend against
ptags that cannot be parsed as keys. It also silently burns mention-cap budget on entries that can never address anyone.I have no evidence of a live exploit — the practical impact I can demonstrate is malformed signed events and wasted cap, not a relay-side vulnerability.
Suggested fix (upstream's call)
Validate each entry through
nostr::PublicKey::from_hexinmention_tagsand return a typed error (e.g. anSdkError::InvalidMentionPubkey { value }, alongside the existingTooManyMentions) rather than silently dropping — a silent drop would make a mention vanish with no feedback to the composer.Two things worth deciding deliberately, which is why I am not sending a patch:
mention_tagsor innormalize_mention_pubkeys. Putting it innormalize_*catches more callers but changes a currently-infallible signature.ptags deliberately, validation would start failing their sends.Happy to send a PR once you have a preference on those two points.