Skip to content

id(directory): format characters (Cf) bypass clean_name/clean_description — U+202E and U+200B pass the 'no control characters' rule for account and group names #1778

Closed

Description

@dezren39

Problem

The directory's name and description cleaners in pkgs/id promise "must not contain control characters", but the check is 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 cleaners, so an account name, a group name, or a group description can carry invisible or text-reordering characters into every view that displays it. This is the same Cf class as #1674, but a different module, different functions, and a different fix: #1674 is scoped to petname.rs (normalize_name/fold/skeleton), and #1770 is scoped to invite.rs, which has no check at all. Neither covers the directory — the layer that creates the alleged names the petname layer then has to defend viewers against.

Evidence (verified this run on main 35726e8e, src/directory.rs sha256 9ef8669c…, identical on sandbox and laptop clones)

  • clean_name (pkgs/id/src/directory.rs:1846-1859): trim, non-empty, ≤64 chars, then !name.chars().any(char::is_control) with error text "name must not contain control characters". Rust's char::is_control is general category Cc only.
  • clean_description (directory.rs:1861-1875): same check (allowing \n), same gap.
  • Call sites: account creation (insert_account, directory.rs:1463, reached from sign_up/sign_up_keyed apply paths), group creation (directory.rs:1588), group update — both the entry point (update_group, directory.rs:862) and the journal apply path (directory.rs:1611). Because the journal apply path re-cleans, a fix here covers replayed entries too.
  • Display surfaces: directory_view.rs:1609 feeds account.name as the alleged name into petname::labels (the viewer-facing contact labels); directory_view.rs:1666,1679 render group and member names; web/explore.rs:399 renders group.description through html_escape, which neutralises markup but not bidi/format characters.
  • Probe (faithful Python mirror of clean_name/clean_description — Rust is_control ≡ category Cc — run on the laptop; probe + log at ~/git/fleet/probe-2026-10-28-laneB-dircf/):
    • Control: Alice<TAB>Admin name → REFUSED (the Cc guard works); LF in a description → accepted, by design.
    • BUG: Alice+U+200B → ACCEPTED; Alice+U+202E+Admin → ACCEPTED; Alice+U+2066+Admin+U+2069 → ACCEPTED; Owners+U+2060 → ACCEPTED; U+FEFF+Alice → ACCEPTED; Al+U+00AD+ice → ACCEPTED; description safe text+U+202E+evil → ACCEPTED. All are category Cf.
    • Plain Alice Smith → accepted, unchanged.

Why it matters

Directory names are self-asserted and shown to other people: account names become the "alleged" half of every contact label, and group names/descriptions are rendered in the directory and web views. An invisible-character account or group name renders identically to a legitimate one (the exact spoof shape the petname module exists to prevent, created here at the source), and a U+202E override in a name or description can reorder the visible text around it. The cleaners' own error messages claim a guarantee they do not deliver.

Acceptance criteria

Dedup

Searched open + recently closed issues for Cf / format character / U+200B / clean_name terms: only #1674 (petname.rs-scoped), #1770 (invite.rs, no Cc check at all), and closed lab-ops-hub UTS-46 items (#652/#516, different package). No filing covers directory.rs clean_name/clean_description.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. added
    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
    priority/normalOrdinary priority (the default)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 28, 2026
  3. dezren39 commented on Oct 28, 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. 165 remaining items

  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked — Drew-owned PR boundary

  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — parked (lane F, task 4889)

  8. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 2, 2026
  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up (ownership correction + CI disposition)

  10. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

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/xsTrivial: expected under 30 minutes 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