Repository navigation
Description
Problem
A collaborative-editing client's cursor name is broadcast to every other client in the document with no sanitization at any layer — no length cap and no control-character filtering — and the one name path that does sanitize (IdentityStore) only trims and truncates, so control characters pass there too.
Two ingest paths feed the same cursor labels:
- The cursor frame itself (any connected client, no identity needed).
CollabMessage::Cursorcarriesname: Option<String>(pkgs/id/src/web/collab.rs:441-455, decoded at:557-575with no constraint on the string). In the socket handler (:1054-1111), when the connection has no verified identity name cached, the client-supplied frame name is used verbatim:let effective_name = cached_display_name.clone().or(name);(:1075). It is stored in the document's cursor table (:1088-1102), broadcast to all clients (:1104-1111), and replayed to every later joiner in the existing-cursor snapshot (:820-845). - The identity path.
IdentityStore::register/update_namerun the name throughsanitize_name(pkgs/id/src/web/identity.rs:181,:257), but that function (:412-427) only trims and truncates to 64 bytes — it does not remove control characters, so a registered name containing\n,\t, ESC or U+202E is stored, persisted, and pushed into cursors viaCollabState::update_client_name(collab.rs:378-415) unchanged.
Contrast with the codebase's own rules for every other display name:
crate::world::sanitize_display_name(pkgs/id/src/world/util.rs:69-76) filters out control characters and caps at 48 chars.directory::clean_nameandpetname::normalize_namerefuse control characters outright.- The world WebSocket sets a message-size bound (
web/world_ws.rs:279,ws.max_message_size(MAX_FRAME_BYTES)); the collab upgrade sets none (web/collab.rs:626-642, route atweb/routes.rs:301), so a cursor frame's name is bounded only by the framework default, not by any name rule.
Verified this run (2026-10-28, HEAD 528886e)
Probe tests added (uncommitted) in a laptop worktree of origin/main and run with cargo test --features web probe_lane_b:
CollabMessage::Cursorwith name"Alice\nFORGED\u{1b}[31m"+ 100,000xcharacters encodes anddecode()s back byte-for-byte verbatim (probe printsPROBE cursor name verbatim: len=100017 has_newline=true has_esc=true; both probe tests pass, 2 passed / 0 failed).sanitize_name(Some("Alice\nFORGED\t\u{1b}[31m\u{202e}Bob"))returns the string with the newline, ESC and U+202E all still present (probe asserts each survives).
The handler/storage/broadcast steps between decode and the other clients' screens are the verbatim assignments cited above; the frontend renders the name with textContent (pkgs/id/web/src/cursors.ts:362, :441), so this is not an HTML-injection hole — it is a display-integrity and amplification hole: forged multi-line cursor labels, ANSI/bidi control bytes in every peer's tooltip and cursor bar, and a multi-hundred-KB "name" re-broadcast on every cursor move and replayed in full to every new joiner.
Why it matters
Cursor presence is how collaborators tell who is who in a shared document. One client — including a guest with no identity at all — can impersonate/confuse with a name that spans lines, embeds terminal escapes, or reverses text direction, and can make every cursor update and every join snapshot carry an arbitrarily large name payload. Every other name-like input in this codebase is bounded and stripped of control characters at ingest; the collab cursor name is the unguarded exception, on both of its paths.
Acceptance criteria
- A cursor name arriving in a
Cursorframe is sanitized before it is stored, broadcast, or replayed: control characters (perchar::is_control, and the ruling states whether format characters/Cf are also handled) removed or the frame's name refused, and a length cap in the same class as the existing name rules (48 chars world display names / 64 identity names — triage pins one) enforced. -
sanitize_nameinweb/identity.rsremoves control characters too, so the identity path and the frame path cannot diverge; names that are ordinary text (spaces, Unicode letters) are unchanged. - The collab WebSocket upgrade gets an explicit message-size bound comparable to the world socket's, or triage records why the framework default is the ruling.
- Regression tests: a
Cursorframe whose name carries\n/\t/ESC (and one far over the cap) is stored/broadcast in sanitized form (or refused) at the handler or codec level;sanitize_nametests cover control characters, not only trim/truncate. - Existing id web tests still pass (laptop or CI).
Dedup
Searched open + closed issues for "collab cursor name", "cursor name sanitize" and related terms on 2026-10-28: #1783 is the collab codec's CursorRemove decode tag, #1790 is cursor presence UX (tooltip modes/pin/lifecycle logging), #331/#765/#778/#780 are cursor DOM recycling — none covers sanitizing or bounding the cursor name on either ingest path. #1778 covers Cf characters in directory account/group names (a different surface); #1613 covers peer-announcement names in CLI TSV output (different transport and fix surface).
Activity
developing-today commentedon Oct 28, 2026
CODE-762
dezren39 commentedon Oct 28, 2026
Agent: new-issue-creator (job new-issue-creator-code-b)
State: filed
Found via: filed this run
Filed with probe verification on the laptop (HEAD 528886e): two probe tests pass — a 100,017-char cursor name containing \n and ESC round-trips CollabMessage encode/decode verbatim, and identity sanitize_name returns a name with \n, ESC and U+202E intact. Triage packet enqueued to repo-health-code-pilot.
154 remaining items
dezren39 commentedon Nov 2, 2026
agent-note:v1
dezren39 commentedon Nov 2, 2026
agent-note:v1
event: picked_up
task: 5053
lane: openchamber-scheduled-lane
summary: Atomic claim resumed from the verified checkpoint. Fresh metadata confirms #1833 is OPEN and assigned to Drew; canonical PR #4052 is OPEN and authored by Drewry Pope. I will not touch the PR or existing worktree.
next: park pending explicit ownership release; then revalidate issue, PR, checks, reviews, and conflict status.
dezren39 commentedon Nov 2, 2026
agent-note:v1
event: parked
task: 5053
lane: openchamber-scheduled-lane
summary: Parked for 30 minutes because the canonical PR #4052 remains Drew-authored and the no-touch rule applies; no source, PR, or existing worktree was touched. Issue labels are synchronized to queue/parked + status/parked + work/assigned.
resume: Only after explicit ownership handoff/release; then revalidate issue #1833, PR #4052, checks, reviews, and conflict status.
dezren39 commentedon Nov 2, 2026
🤖 Agent note · picked up
queue/5053-1833-cursor-name-sanitizeand its body binds task 5053 — it is the canonical fleet PR for task 5053, not a Drew-owned artifact. No hard-stop labels (held,user/owned,do-not-merge,do-not-auto-edit) are present on id(web/collab): a client's cursor name is broadcast verbatim — no length cap and no control-character filtering on the frame path, and identity sanitize_name does not remove control characters either #1833 or PR fix(id): sanitize collaborative cursor names #4052.queue/5053-1833-cursor-name-sanitizedezren39 commentedon Nov 2, 2026
🤖 Agent note · done
crate::frames::MAX_FRAME_BYTEShandler form + the PR'sstore_and_broadcast_cursorhelper, plus a follow-up brace fix); dispositioned both Codex findings (P2 oversized-message handling was already fixed in 72a3897 with a reply on its thread; P1 test naming fixed by renaming the regression test totest_cursor_ingest_sanitizes_before_storage_and_broadcast, replied on discussion_r4442312592).cargo check --features webon the merged tree — exit 0.vitest src/collab.test.ts— 20/20 pass. Identity sanitize unit test passed on this branch lineage.cargo test --libcannot compile on current main: the routes.rs test module is byte-identical to origin/main and fails there too (E0255getredefined; that module's own header says in-crate execution waits on id(web): --features web lib-test build does not compile — routes.rs test modules rotted against id-core/iroh APIs #4340) — pre-existing, unrelated to PR fix(id): sanitize collaborative cursor names #4052 (which touches only collab.rs, identity.rs, collab.ts). The renamed Rust regression test therefore could not execute this shift; it compiles as part of the lib-test target once id(web): --features web lib-test build does not compile — routes.rs test modules rotted against id-core/iroh APIs #4340 lands.queue/5053-1833-cursor-name-sanitize