Skip to content

Close the system-message spoofing hole in the action parser - #1662

Open
jackjackbits wants to merge 3 commits into
feat/identity-changed-warningfrom
fix/system-message-spoofing
Open

Close the system-message spoofing hole in the action parser#1662
jackjackbits wants to merge 3 commits into
feat/identity-changed-warningfrom
fix/system-message-spoofing

Conversation

@jackjackbits

Copy link
Copy Markdown
Collaborator

Tier-1 batch 4/6. processActionMessage rewrote the sender to "system" — the formatter's trusted styling — for ANY * … * content containing 🫂, 🐟, or the substring "took a screenshot", so a hostile peer could render arbitrary text as a system-authored line (* SECURITY: your session key expired, re-verify at evil.example — bob took a screenshot *). Only the exact locally-generatable action shapes qualify now, and the actor slot must equal the actual wire sender — a peer can hug/slap/screenshot only as themselves; the target slot is bounded, single-line, nickname-shaped. Same templates iOS and Android emit, so legit actions render unchanged; everything else falls through as an ordinary peer message under the sender's real name. Pinned by tests incl. the original spoof payload and actor-mismatch cases. No new strings.

🤖 Generated with Claude Code

processActionMessage rewrote the sender to "system" — the formatter's
trusted styling — for ANY `* … *` content containing 🫂, 🐟, or the
substring "took a screenshot". A hostile peer could render arbitrary
text as a system-authored line: `* SECURITY: your session key expired,
re-verify at evil.example — bob took a screenshot *`.

Only the exact locally-generatable action shapes qualify now, and the
actor slot must equal the actual wire sender: a peer can hug, slap, or
screenshot only as themselves, and the target slot is bounded,
single-line, and nickname-shaped. Same templates iOS and Android emit,
so legit actions render unchanged; anything else falls through as an
ordinary peer message under the sender's real name.

Pinned by tests covering the legit shapes, the original spoof payload,
actor-mismatch, and free-text-in-target-slot cases. No new strings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a05c67252f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +862 to +865
guard inner.hasPrefix(prefix), inner.hasSuffix(suffix),
inner.count >= prefix.count + suffix.count + 1 else { return false }
let target = inner.dropFirst(prefix.count).dropLast(suffix.count)
return !target.isEmpty && target.count <= 64 && !target.contains("\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate targets before assigning the system sender

For an inbound public message such as * 🫂 bob hugs SECURITY: session expired—visit evil.example *, this check accepts the entire short, single-line payload as the target even though it is not a resolved nickname, after which processActionMessage rewrites the sender to system. Because CommandProcessor.handleEmote only generates actions for nicknames resolved by getPeerIDForNickname, length and newline checks do not establish an exact locally generated shape; the system-message spoofing path therefore remains available with an action preamble.

Useful? React with 👍 / 👎.

Comment on lines +837 to +840
let isActionMessage =
Self.matchesActionTemplate(inner, prefix: "🫂 \(sender) hugs ", suffix: "")
|| Self.matchesActionTemplate(inner, prefix: "🐟 \(sender) slaps ", suffix: " around a bit with a large trout")
|| inner == "\(sender) took a screenshot"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept the suffixed sender used in location chats

In location channels, CommandProcessor.handleEmote embeds the unsuffixed context.nickname in /hug and /slap content, while NostrInboundPipeline sets message.sender through displayNameForNostrPubkey, which appends #<pubkey suffix>. Thus an ordinary generated action such as * 🫂 bob hugs alice#1234 * arrives with a sender like bob#ab12 and fails these actor-anchored templates, regressing every received location-chat action to ordinary peer text instead of system-action rendering.

Useful? React with 👍 / 👎.

jackjackbits and others added 2 commits August 11, 2026 10:10
- Codex P1: bounded-single-line was not "exact locally generated
  shape" — a self-attributed action could smuggle a preamble into the
  target slot ("… hugs SECURITY: reset your keys at evil…"). The
  target must now be a single name token ("you" or a whitespace-free
  ≤32-char nickname, optionally #abcd-suffixed); free text with spaces
  degrades to a plain message.
- Codex P1: location-channel senders arrive suffixed (bob#ab12) while
  handleEmote embeds the unsuffixed nickname, so the actor match
  regressed every received geohash action to plain text. The actor is
  now compared by base name (splitSuffix), restoring legit rendering.

Tests cover the suffixed-sender case, the suffixed target, and the
preamble spoof.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jackjackbits

Copy link
Copy Markdown
Collaborator Author

Both Codex findings addressed:

  • Regression (suffixed senders): the actor is now matched by base name via splitSuffix, so a location-channel action from bob#ab12 with unsuffixed content bob hugs alice renders as a system action again. Test added.
  • Residual spoof (target preamble): the target slot must now be a single name token — "you" or a whitespace-free ≤32-char nickname (optionally #abcd) — so bob hugs SECURITY: reset your keys at evil.example no longer qualifies (spaces). A space-containing nickname degrades to a plain message, the safe direction. Test added.

@Chessing234 Chessing234 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the actor-anchoring is the right shape and the template match kills the preamble case. but the target slot still admits the payload your own example is built around.

isNameToken accepts any whitespace-free token up to 32 chars. https://evil.tld is 16 chars with no whitespace, so:

* 🫂 <attacker's own nick> hugs https://evil.tld *

passes matchesActionTemplate, the actor equals the wire sender, and it renders as system. and the content body isn't skipped for system senders — formatMessage's sender != "system" guard only skips the <@name> prefix, the match loop below still runs — so hasURLHint fires on :// and the token comes out blue, underlined, with matchStyle.link set. attacker-chosen tappable link under trusted styling, one packet, no preamble needed.

www.evil.tld works the same way via the www. hint.

the bound that would hold is the nickname charset rather than "no spaces": handleEmote only ever puts a resolved nickname or you there, and nicknames don't contain : or /. rejecting those two characters closes it; matching the actual #abcd suffix shape closes it properly.

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.

2 participants