Skip to content

fix(sdk): reject malformed mention pubkeys before signing p tags - #6372

Draft
holmes wants to merge 1 commit into
block:mainfrom
holmes:rust-worker/validate-mention-pubkeys-sdk
Draft

fix(sdk): reject malformed mention pubkeys before signing p tags#6372
holmes wants to merge 1 commit into
block:mainfrom
holmes:rust-worker/validate-mention-pubkeys-sdk

Conversation

@holmes

@holmes holmes commented Aug 20, 2026

Copy link
Copy Markdown

Summary

buzz_sdk::builders::mention_tags lowercased whatever string it was handed and pushed it straight into a p tag, 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 as p tags by build_message and build_forum_post.

Fixes #6291

Acceptance criteria → evidence

Following @kiranmagic7's recommendation on the issue:

# Criterion Evidence
1 Validate in the private mention_tags, not the public normalize_mention_pubkeys crates/buzz-sdk/src/builders.rs — the single changed line inside fn mention_tags. normalize_mention_pubkeys is untouched, so no public signature changes.
2 Reuse the existing check_pubkey_hex helper let lower = check_pubkey_hex(hex, "mention pubkey")?; replaces hex.to_ascii_lowercase(). No new helper added.
3 Reuse the existing SdkError::InvalidInput check_pubkey_hex already returns InvalidInput; no new error variant is introduced.
4 Cover all 3 builders malformed_mention_pubkeys_are_refused_by_every_builder asserts against build_message, build_forum_post, and build_forum_comment — all three share mention_tags.
5 Reject empty / non-hex / 63-char / 65-char Same test's table: "", "not-a-pubkey", "zzzz", "../../etc/passwd", "a"*63, "a"*65, plus "z"*64 and a 64-char path-like string so a length-only check cannot pass.
6 Preserve valid uppercase canonicalization valid_mentions_are_canonicalized_and_deduped_across_case — uppercase hex is accepted and the emitted p tag is lowercase, across all three builders.
7 Preserve valid dedupe Same test: the same pubkey in both spellings collapses to exactly one p tag. Pre-existing message_mentions_deduped still passes.
8 Preserve TooManyMentions precedence for >50 too_many_mentions_takes_precedence_over_validation — a 51-entry list that also contains a malformed entry still returns TooManyMentions. The cap check stays ahead of validation. mentions_at_the_cap_still_sign pins that the boundary itself did not move.
9 Inspect #3036 overlap; keep the patch narrow See the compatibility note below.
10 Make these tests actually gate 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 of check_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_tags

mention_tags is the single choke point every mention reaches, shared by all three builders, and callers can hand those builders an explicit list without ever going through normalize_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_tags already validates with check_pubkey on 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-sdk to just test-unit and the mirrored scripts/run-tests.sh fallback is a CI-behavior change, so it deserves its own justification: without it the regression tests above would never run in CI. buzz-sdk is a workspace member, so it was getting clippy and check, but no CI job executed a single one of its tests. This is the exact hazard the Justfile already warns about for other crates — "nothing in CI runs cargo 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:

  • Negative control — with the runner files at pristine main and a deliberately panicking buzz-sdk test in the tree, just test-unit exited 0 and printed "All tests passed!". The crate name never appeared in the log.
  • Positive control — same sabotaged test, with only these two runner edits applied: just test-unit exited 1 with buzz-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 edits mention_tags: it adds parent_author: Option<PublicKey> to ThreadRef, emits a parent-author p tag in thread_tags, and seeds mention_tags's dedup set from p tags already on tags. This PR replaces only the let lower = ... line inside the loop, so the two changes are compatible in substance. Mechanically, whichever lands second needs the trivial merge fixup: #3036 adds parent_author: None to every ThreadRef literal, 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: None additions. Merging #3036 into this head also produces one textual conflict in crates/buzz-sdk/src/builders.rs, confined to the mention_tags doc 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 the let lower = ... line and #3036 changes the seen initializer. For reference, merging #3036 into plain main leaves builders.rs auto-merging cleanly, so this conflict is specific to the combination. Once the doc comment is resolved and parent_author: None is added to the three new ThreadRef literals here, buzz-sdk compiles and the only failing test is #3036's own self_reply_drops_the_parent_p_tag — which fails identically with #3036 merged into main without this PR, because build_message gained .allow_self_tagging() in #4975 after #3036's merge base.

Test plan

  • cargo test -p buzz-sdk267 passed, 0 failed (5 new tests; pre-existing message_mentions_deduped, message_too_many_mentions, and the three *_preserves_self_mention_p_tag tests still green)
  • cargo fmt --all -- --check — clean
  • just clippy (workspace, -D warnings) — clean
  • just test-unit — all 10 steps pass, now including buzz-sdk
  • just file-size-check — pass
  • just desktop-tauri-fmt-check / desktop-tauri-check / desktop-tauri-test — pass (2684 tests; desktop/src-tauri consumes buzz-sdk)
  • Full 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 here

All 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_hex alone is the right call for now.

`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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

build_message signs mention pubkeys that are not valid hex public keys

1 participant