fix(sdk): reject malformed mention pubkeys before signing p tags - #6372
Draft
holmes wants to merge 1 commit into
Draft
fix(sdk): reject malformed mention pubkeys before signing p tags#6372holmes wants to merge 1 commit into
holmes wants to merge 1 commit into
Conversation
`builders::mention_tags` lowercased whatever it was handed and pushed it straight into a `p` tag, so `""`, `"not-a-pubkey"`, `"../../etc/passwd"` and 63/65-character strings were all signed onto real events. A composer that mentioned someone this way looked successful while notifying nobody, and the malformed tag propagated to every relay and client that read it. Validate with the existing `check_pubkey_hex` inside `mention_tags`. That helper is the SDK's established structural pubkey contract (64 ASCII hex, returned lowercased) and already guards the other builders that emit `p` tags, so mentions now agree with the rest of the module and surface the existing `SdkError::InvalidInput`. Desktop's own `events.rs::mention_tags` already applies the identical rule, so this aligns the SDK with shipped behavior rather than inventing a new one. The check goes in the private `mention_tags` rather than the public `normalize_mention_pubkeys`: `mention_tags` is the single choke point shared by `build_message`, `build_forum_post` and `build_forum_comment`, and callers can hand those builders an explicit list without ever going through the normalizer. Fixing it here closes that bypass without changing a public signature. One malformed entry refuses the whole list instead of being filtered out, because a silently dropped mention is the failure being fixed. The cap check stays ahead of validation so an over-cap list still reports `TooManyMentions` rather than having that error masked by whichever entry happens to be malformed; both behaviors are pinned by tests. Adopting `nostr::PublicKey::from_hex`'s stricter on-curve check was considered and left out: it would change every `check_pubkey_hex` call site, which is a repo-wide consistency decision rather than part of this fix. Also enumerate `buzz-sdk` in `just test-unit` and the `run-tests.sh` fallback. The crate is a workspace member, so it was getting clippy and check, but no CI job ran a single one of its tests — verified with a control: a deliberately panicking buzz-sdk test still let `just test-unit` exit 0 and report "All tests passed!" before this change, and fails it after. Without those two mirrored entries the regression tests added here would never gate anything. Fixes block#6291 Signed-off-by: rust worker <cb8459a6e4c936835834cef03b8331fe806108dd03da97d39c7036a7d710e860@buzz.block.builderlab.xyz> Co-authored-by: Jason Holmes <holmes@squareup.com> Signed-off-by: Jason Holmes <holmes@squareup.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
buzz_sdk::builders::mention_tagslowercased whatever string it was handed and pushed it straight into aptag, so malformed mention pubkeys were signed onto real events. A composer that mentioned someone this way looked successful while notifying nobody, and the bad tag propagated to every relay and client that read it.Reproduced before fixing:
"","not-a-pubkey","zzzz","../../etc/passwd", and 63/65-character strings were all accepted and emitted verbatim asptags bybuild_messageandbuild_forum_post.Fixes #6291
Acceptance criteria → evidence
Following @kiranmagic7's recommendation on the issue:
mention_tags, not the publicnormalize_mention_pubkeyscrates/buzz-sdk/src/builders.rs— the single changed line insidefn mention_tags.normalize_mention_pubkeysis untouched, so no public signature changes.check_pubkey_hexhelperlet lower = check_pubkey_hex(hex, "mention pubkey")?;replaceshex.to_ascii_lowercase(). No new helper added.SdkError::InvalidInputcheck_pubkey_hexalready returnsInvalidInput; no new error variant is introduced.malformed_mention_pubkeys_are_refused_by_every_builderasserts againstbuild_message,build_forum_post, andbuild_forum_comment— all three sharemention_tags."","not-a-pubkey","zzzz","../../etc/passwd","a"*63,"a"*65, plus"z"*64and a 64-char path-like string so a length-only check cannot pass.valid_mentions_are_canonicalized_and_deduped_across_case— uppercase hex is accepted and the emittedptag is lowercase, across all three builders.ptag. Pre-existingmessage_mentions_dedupedstill passes.TooManyMentionsprecedence for >50too_many_mentions_takes_precedence_over_validation— a 51-entry list that also contains a malformed entry still returnsTooManyMentions. The cap check stays ahead of validation.mentions_at_the_cap_still_signpins that the boundary itself did not move.Justfile+scripts/run-tests.sh— see "Why the runner change is here".Out of scope by design: switching to
nostr::PublicKey::from_hex. Its stricter on-curve rule would change every one ofcheck_pubkey_hex's ~26 call sites, which is a repo-wide consistency decision rather than part of this fix. The doc comment records that reasoning.Why validation lives in
mention_tagsmention_tagsis the single choke point every mention reaches, shared by all three builders, and callers can hand those builders an explicit list without ever going throughnormalize_mention_pubkeys. Validating here closes that bypass without breaking the normalizer's public API.This also aligns the SDK with behavior already shipping elsewhere: desktop's own
events.rs::mention_tagsalready validates withcheck_pubkeyon the identical structural rule (64 ASCII hex). The SDK was the inconsistent one.One malformed entry refuses the whole list rather than being filtered out — a silently dropped mention is precisely the failure being fixed, and filtering would preserve it.
Why the runner change is here
Adding
buzz-sdktojust test-unitand the mirroredscripts/run-tests.shfallback is a CI-behavior change, so it deserves its own justification: without it the regression tests above would never run in CI.buzz-sdkis a workspace member, so it was gettingclippyandcheck, but no CI job executed a single one of its tests. This is the exact hazard theJustfilealready warns about for other crates — "nothing in CI runscargo test --workspace— workspace membership alone buys clippy/check, not a single executed test."Verified with a positive/negative control rather than by reading the config:
mainand a deliberately panickingbuzz-sdktest in the tree,just test-unitexited 0 and printed "All tests passed!". The crate name never appeared in the log.just test-unitexited 1 withbuzz-sdk tests FAILED.The sabotage test was removed afterward; it is not part of this PR. Both entries are needed because the two lists (nextest path and the no-nextest fallback) must stay in step. This is scoped strictly to making the crate's existing tests execute — no other CI behavior is touched.
Compatibility with #3036
#3036 (
fix/nip10-reply-p-tags) also editsmention_tags: it addsparent_author: Option<PublicKey>toThreadRef, emits a parent-authorptag inthread_tags, and seedsmention_tags's dedup set fromptags already ontags. This PR replaces only thelet lower = ...line inside the loop, so the two changes are compatible in substance. Mechanically, whichever lands second needs the trivial merge fixup: #3036 addsparent_author: Noneto everyThreadRefliteral, including the ones in the tests added here.One correction to the paragraph above, from an independent merge trial: the fixup is not only the
parent_author: Noneadditions. Merging #3036 into this head also produces one textual conflict incrates/buzz-sdk/src/builders.rs, confined to themention_tagsdoc comment — both branches rewrote it, this PR to explain the validation and #3036 to explain the seeded dedup set. The resolution is to keep both blocks; there is no logic conflict, since this PR changes thelet lower = ...line and #3036 changes theseeninitializer. For reference, merging #3036 into plainmainleavesbuilders.rsauto-merging cleanly, so this conflict is specific to the combination. Once the doc comment is resolved andparent_author: Noneis added to the three newThreadRefliterals here,buzz-sdkcompiles and the only failing test is #3036's ownself_reply_drops_the_parent_p_tag— which fails identically with #3036 merged intomainwithout this PR, becausebuild_messagegained.allow_self_tagging()in #4975 after #3036's merge base.Test plan
cargo test -p buzz-sdk— 267 passed, 0 failed (5 new tests; pre-existingmessage_mentions_deduped,message_too_many_mentions, and the three*_preserves_self_mention_p_tagtests still green)cargo fmt --all -- --check— cleanjust clippy(workspace,-D warnings) — cleanjust test-unit— all 10 steps pass, now includingbuzz-sdkjust file-size-check— passjust desktop-tauri-fmt-check/desktop-tauri-check/desktop-tauri-test— pass (2684 tests;desktop/src-tauriconsumesbuzz-sdk)just ci— the remaining steps are JS/Dart (desktop-test,desktop-build,web-build,mobile-test); this change touches only Rust + two runner scripts, so CI covers them hereAll gate results above were confirmed at commit
5fe5929de39388cada354593ac875d3c42c3d830.Draft for maintainer review on two points: whether you want the runner entries in this PR or split out, and confirmation that leaving
PublicKey::from_hexalone is the right call for now.