fix(security): close S3 (arg-injection), S4 (presence-name), S1 (panic-slice)

Three findings from the first security pass:

- S3 (Medium): the peer-supplied screen-share ticket was passed to pixelpass
  as the first positional CLI arg with no end-of-options guard, so a ticket
  starting with `-`/`--` could be reinterpreted as a flag (argument injection).
  New pure `viewer_args()` puts flags first, then a `--` guard, then the ticket
  positionally; spawn_viewer uses it. +2 tests.

- S4 (Medium): peer presence display-names (gossip `Announce`, untrusted and
  spoofable) were rendered unsanitized/unbounded, unlike the chat path. New
  `sanitize::sanitize_name` strips bidi/zero-width format chars + control chars,
  collapses whitespace, and caps at 48 chars; applied at the gossip ingest point
  so every consumer gets a safe value. +4 tests.

- S1 (Low): `&id[..8]` byte-slices could panic on a short/non-ASCII id. New
  panic-free `short_id()` (char-based take) replaces both slices. +1 test.

158 lib tests (was 151), clippy --all-targets clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 21:16:24 -04:00
co-authored by Claude Opus 4.8
parent 420535c5d3
commit fe627166d5
5 changed files with 148 additions and 6 deletions
+5 -1
View File
@@ -144,7 +144,11 @@ impl RoomState for IrohGossipState {
crate::log_msg(&format!("Gossip Event::Received from author={:?}, payload={:?}", payload.author, payload.msg));
match payload.msg {
GossipMessage::Announce(state) => {
GossipMessage::Announce(mut state) => {
// Presence names are untrusted (and author-
// spoofable): sanitize at ingest so every
// consumer gets a safe value (security S4).
state.name = crate::sanitize::sanitize_name(&state.name);
let (is_new, state_changed) = {
let mut peer_map = peers.lock().unwrap();
let is_new = !peer_map.contains_key(&payload.author);