Skip to content

id(world): sanitize_display_name / sanitize_chat filter Cc only — Cf format characters (U+202E, U+200B, bidi isolates) pass into participant names and broadcast chat, and a zero-width-only name passes the join empty check #2112

Closed

Description

@dezren39

Problem

The world runtime's two text sanitizers promise to strip control characters from participant display names and chat text, but they filter with char::is_control — Unicode category Cc only. Category-Cf format characters (U+200B ZERO WIDTH SPACE, U+202E RIGHT-TO-LEFT OVERRIDE, U+2066/U+2069 bidi isolates, U+2060 WORD JOINER, U+FEFF, U+00AD SOFT HYPHEN) pass both sanitizers untouched, so a participant can join with an invisible or bidi-reordering display name, and chat lines broadcast to every other participant can carry the same characters. A display name consisting solely of zero-width spaces also passes the join path's non-empty check, so a participant can be present under a literally invisible name.

This is the same Cf class as #1674 (petname), #1778 (directory) and #1770 (invite), but a different module and fix surface: none of those covers pkgs/id/src/world/util.rs or the world join/chat paths in world/core.rs. #1833 covers web/collab cursor names, a different subsystem.

Evidence (verified this run on main 42796be5)

  • sanitize_display_name (pkgs/id/src/world/util.rs:69-76): .filter(|ch| !ch.is_control()), then take(48), then trim. Rust's char::is_control is general category Cc only.
  • sanitize_chat (pkgs/id/src/world/util.rs:78-85): same filter (allowing \n/\t), then trim.
  • Call sites: join — world/core.rs:847 sanitizes the display name, core.rs:848-851 refuses only if the sanitized name is empty; chat — world/core.rs:955 (participant chat) and core.rs:578 (host/system chat) sanitize before the event is committed and broadcast.
  • Laptop probe (verbatim copies of both functions, main 42796be5, world/util.rs sha256 6c6cab01…; probe ~/git/fleet/probe-2026-10-28-laneB-worldcf/):
    • "Ann\u{202E}nimdA" → name and chat output unchanged (RLO survives).
    • "\u{200B}\u{200B}" (zero-width only) → output unchanged and non-empty, so the join empty-name refusal does not fire.
    • "Ann\u{200B}", "An\u{00AD}n", "\u{2060}\u{FEFF}Bo", chat "hi \u{2066}evil\u{2069}" and "pay \u{202E} 100" → all unchanged.
    • Control: Cc U+0007 is stripped from both ("A\u{0007}nn" → "Ann"), as documented.
    • is_control() returns false for U+200B, U+202E, U+2066, U+2060, U+FEFF and U+00AD.

Why it matters

Display names and chat are the identity other participants see inside a shared world. A RIGHT-TO-LEFT OVERRIDE in a name or chat line reorders the text around it for every viewer (the classic spoofing primitive), zero-width characters make two visually identical names distinct (or one name invisible), and bidi isolates can hide or fake quoted text. The sanitizers are the single choke point for both surfaces, so this is the layer where the Cf class has to be closed for worlds — the directory/petname fixes do not reach it.

Acceptance criteria

Activity

  1. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 29, 2026
  3. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  5. 3 remaining items

  6. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 1, 2026
  7. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    event: picked_up
    task: 5089 (repo-health-code-pilot, OpenChamber scheduled lane)
    state: Fresh GitHub shows #2112 OPEN, no held/do-not-edit labels, and no matching open implementation PR. I will reuse the existing petname Cf classifier as pinned by triage 3074.

  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  9. added a commit that references this issue on Nov 2, 2026
  10. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

  11. added a commit that references this issue on Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions