Skip to content

fix(id/peers): escape announced names at the printers so a gossip name cannot forge TSV rows (#1613) - #1798

Merged
dezren39 merged 2 commits into
mainfrom
queue/1856-1613-peers-name-escaping
Oct 30, 2026
Merged

dezren39 merged 2 commits into
mainfrom
queue/1856-1613-peers-name-escaping

Conversation

@dezren39

@dezren39 dezren39 commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Problem

id peers and both REPL peer printers interpolate a peer's announced name raw into a tab-separated, one-row-per-peer row. The name comes from an unsigned PeerAnnouncement broadcast on a public gossip topic: it is postcard-decoded with no validation, stored verbatim by PeerDiscovery::update, and served verbatim by ListPeers. A name containing LF forges extra peer rows (including a fully-formed fake node row); a name containing TAB shifts every following column.

Print sites fixed:

  • pkgs/id/src/commands/peers.rs — cmd_peers row
  • pkgs/id/src/commands/repl.rs — REPL peers
  • pkgs/id/src/commands/repl.rs — REPL peers_on_node

Ruling (acceptance 3)

The rendering contract is pinned at the printers, not at ingest or serving. Stored and served announcements stay byte-for-byte verbatim (ListPeers is unchanged, no ingest validation added): a store may already hold a sync-ingested value that ingest validation could not retroactively clean, and protocol data is data. The printers never let a name break row structure. This is the same ruling the id claim / id doc / id group plain-text renderer fix (#1611) pinned.

The escaping scheme (acceptance 2)

One shared helper, plain_field in the new pkgs/id/src/commands/text.rs, is used by all three sites (the two REPL sites share format_peer_line, which calls it):

Trigger set, stated in the helper's docs: any control character (this covers LF, CR, TAB, NUL, DEL, NEL, ...) and the Unicode line/paragraph separators U+2028 / U+2029.

One deliberate difference from #1611's key=value renderer, called out here because the two schemes must not drift silently: #1611 also quotes on an ASCII space (a space breaks its key=value tokenisation), while this helper does not — a space is not a TSV field delimiter, and the issue's acceptance requires ordinary names with spaces to display unchanged. The mechanism and the escaping convention are identical; only the trigger set is narrowed to what can break a tab-separated row.

Cross-links: #1611 (sibling renderer ruling), and the #1613 triage ruling on the issue (task 1723) which pinned the contract at the renderer.

Tests (acceptance 1, 4)

  • commands/text.rs: unit tests pin the rule at the helper — byte-identical for space-free and space-bearing ordinary names, quoted/escaped for LF, CR, TAB, other controls and U+2028/U+2029, no raw structure byte in the output, forged payload cannot add a row.
  • commands/peers.rs: format_peer_row tests (one row, four fields, raw name never reaches the row; ordinary names unchanged; - when the name is absent) plus a round-trip test: forged names go through postcard gossip ingest -> PeerDiscovery::update -> ListPeers response (built exactly as protocol.rs builds it) -> postcard wire -> both printers; asserts one row per peer per printer, four fields per row, and that stored/served names are still verbatim.
  • commands/repl.rs: format_peer_line tests for the same names (it is the single helper behind both REPL sites).

Verification

  • cargo test --lib commands:: -> 99 passed, 0 failed.
  • cargo test --lib (full default-features suite) -> 1487 passed, 1 failed: log_sync::tests::test_batch_collection_refused_stream_ends_at_the_total_cap (203 vs 256), unrelated and flaky — it passes when rerun alone.
  • cargo test --doc commands::text -> passed. cargo fmt --check -> clean.
  • cargo test --all-features cannot compile on this laptop: the web feature needs built web assets (src/web/assets.rs RustEmbed) and src/web/mod.rs fails with Assets::get/Assets::iter not found. Pre-existing environment limitation, unrelated to this change; tests were run with default features.

Closes #1613

…e cannot forge TSV rows (#1613)

A peer's announced name arrives over unsigned gossip, is postcard-decoded
with no validation, stored verbatim by PeerDiscovery::update and served
verbatim by ListPeers. All three tab-separated printers interpolated it
raw, so a name with LF forged extra peer rows (including a fully-formed
fake node row) and a name with TAB shifted every following column.

Pin the contract at the printers, as #1611's triage pinned it for the
claim/doc/group plain-text output: stored and served announcements stay
verbatim, and no ingest validation is added (a store may already hold a
sync-ingested value ingest cannot retroactively clean).

Add one shared helper, commands::text::plain_field, used by all three
sites (the two REPL sites share format_peer_line). A value with no
structure-breaking character is returned byte-identically; a value that
has one is rendered as a double-quoted JSON string literal with the
offending characters escaped — the same convention #1611's renderer
uses. The trigger set is any control character (covers LF, CR, TAB, NUL,
DEL, NEL, ...) plus U+2028/U+2029. #1611's key=value renderer also
quotes on an ASCII space, which breaks its tokenisation; a space does not
delimit a TSV field and ordinary names with spaces must display
unchanged, so space stays raw here. Mechanism and escaping are shared;
only the trigger set is narrowed to what can break a tab-separated row.

Tests: helper unit tests; format_peer_row / format_peer_line tests for
LF, CR, TAB, control-only and forged names and for ordinary (spaces,
Unicode) names; and a round-trip test that drives forged names through
postcard gossip ingest -> PeerDiscovery::update -> the ListPeers response
-> the wire -> both printers, asserting one row and four fields per peer
while the served names stay verbatim.

cargo test --lib (default features): 1488 passed, 1 flaky log_sync cap
test that passes on rerun; cargo fmt --check clean.
@dezren39
dezren39 requested a review from a team as a code owner October 28, 2026 20:02
@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 30 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1abf1f9d-11ba-41e8-b5cc-a0a802275752
Commits

Reviewing files that changed from the base of the PR and between e91098f and 0c686f0.

Files selected for processing: 4
  • pkgs/id/src/commands/mod.rs
  • pkgs/id/src/commands/peers.rs
  • pkgs/id/src/commands/repl.rs
  • pkgs/id/src/commands/text.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T20:06:20.059162Z 0c686f0 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@dezren39 dezren39 closed this Oct 30, 2026
@dezren39 dezren39 reopened this Oct 30, 2026
@dezren39
dezren39 added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 47f2725 Oct 30, 2026
52 of 63 checks passed
@dezren39
dezren39 deleted the queue/1856-1613-peers-name-escaping branch October 30, 2026 03:33
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.

id(peers): a peer's announced name is printed raw — a newline in a gossip announcement forges extra TSV rows in id peers and the REPL

1 participant