Skip to content

id(petname): format characters (Cf) bypass both petname guards — U+200B makes a spoof of another contact's name, U+202E passes the 'no control characters' rule #1674

Closed

Description

@dezren39

Summary

pkgs/id petnames exist so a contact can never be mistaken for someone else: the module doc (src/petname.rs:1-7) says the store "refuses a petname that could be taken for someone else's name", and normalize_name promises a petname "cannot contain control characters". Both promises are defeated by Unicode format characters (general category Cf):

  • normalize_name (src/petname.rs:217-230) refuses a name when name.chars().any(char::is_control). Rust's char::is_control is category Cc only. Category-Cf characters — U+202E RIGHT-TO-LEFT OVERRIDE, U+200B ZERO WIDTH SPACE, U+2060 WORD JOINER, U+FEFF, U+00AD SOFT HYPHEN — pass.
  • set()'s confusability checks (src/petname.rs:107-135) compare fold() (lowercase) and skeleton() (look-alike map + rn→m, vv→w) verbatim. Neither strips Cf characters, so an invisible suffix changes both strings while changing nothing on screen.

Verified shapes (main d39e6111, src/petname.rs sha256 ff6cba65)

Premise exercised this run with a faithful mirror of normalize_name/fold/skeleton/set (probe folder: probe-2026-10-28-laneB-petnamecf/premise.py + premise.log):

  • CTRL: Pay<TAB>Pal → refused by normalize (Cc). PayPa1 vs alleged PayPal → refused as confusable. Both guards work for the shapes they cover.
  • BUG-A (spoof): petname PayPal + U+200B for a different contact, while another contact's alleged name is PayPal → ACCEPTED (fold unequal, skeleton unequal). The two names render identically in the directory view (src/directory_view.rs builds its labels from petname::labels), so the viewer sees two contacts apparently named exactly "PayPal" — precisely the impersonation shape the module exists to prevent.
  • BUG-B (bidi): a petname containing U+202E is ACCEPTED by normalize_name, despite its documented "cannot contain control characters" rule. A right-to-left override in a displayed label (Label::text, src/petname.rs:285-295) can reorder the visible text around it.
  • BUG-C: PayPal + U+2060 WORD JOINER → also ACCEPTED against alleged PayPal.

Existing tests (src/petname.rs mod tests) pin the Cc tab case and the PayPa1 skeleton case, but no Cf character appears anywhere in the file's tests.

Disclosure

The prepared Rust probe (probe_laneb_test.rs, same probe folder: asserts Cf names are refused by normalize_name and that the BUG-A spoof is refused by set()) was NOT executed this run — the laptop is unreachable (ongoing fleet-wide SSH outage) and verification probes are never compiled on the sandbox. Triage: run the probe first; it should currently fail on the BUG asserts. The line trace and the premise mirror above are the evidence this filing rests on.

Why it matters

Petnames are a trust surface: they are how a viewer tells contacts apart when account names are self-asserted ("Account names are alleged"). An invisible-character spoof defeats the one check standing between an alleged name and a look-alike petname, and a bidi override in a label can misrender who is who. The fix is small and local to petname.rs.

Acceptance criteria

  • normalize_name refuses (or strips, with the choice pinned and documented) Unicode format characters (category Cf) — at minimum U+200B, U+202E/U+202A–U+202E, U+2060, U+FEFF, U+00AD — so the "no control characters" promise matches what callers and readers believe it says.
  • The confusability comparison in set() cannot be bypassed by invisible characters: PayPal+ZWSP against alleged PayPal is refused (by the normalize refusal, an explicit Cf-strip in fold/skeleton, or both — triage pins which layer owns it).
  • Tests cover at least: ZWSP suffix spoof vs an alleged name, RLO inside a name, and a Cf character against an existing petname (not only against alleged names).
  • Existing petname tests still pass; legitimate names with ordinary punctuation/spaces are unaffected.

Related but distinct: #1499 (torn final line in petnames.jsonl) is the same file's crash-recovery defect — different function, different fix.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    kind/securitySecurity issue or security hardening
    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)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    agent: new-issue-creator-code-b
    state: filed
    Found via: filed this run (2026-10-28 lane-B creator run) after a line-by-line trace of src/petname.rs on main d39e611 (sha256 ff6cba65) and a premise exercise mirroring normalize_name/fold/skeleton/set (hidden_files/probe-2026-10-28-laneB-petnamecf/premise.py + premise.log): Cc controls and the pinned PayPa1 skeleton case are refused (controls), while PayPal+U+200B against alleged PayPal, PayPal+U+2060, and a name containing U+202E are all ACCEPTED. Disclosure: the prepared Rust probe (probe_laneb_test.go, same folder) was NOT executed — the laptop is unreachable this run (ongoing outage) and probes are never compiled on the sandbox; triage should run the probe first and narrow this issue if any verdict differs. Deliberately NOT self-solved: whether Cf handling lives in normalize_name, in fold/skeleton, or both is a small contract pin for queue-lane triage, and the laptop worktree + PR + CI cycle is unavailable while the laptop is unreachable. Distinct from #1499 (same file, torn-journal recovery — different function and fix).

  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Oct 31, 2026
  4. 34 remaining items

  5. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

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

    @dezren39
    MemberAuthor

    agent-note:v1 — merging (queue task 4240, lane A resume)

    Attribution for dispatch run 41204856358 (head 5176ab3) is complete:

    • All petname tests pass, including the new Cf tests a_format_character_spoof_is_refused_by_set and a_stored_format_character_name_cannot_spoof_via_comparison (test-unit: 1989 passed on this branch vs 1984 on main — the +5 are this PR's tests).
    • Every failing basic job (clippy, cargo-fmt, rustfmt, treefmt, web-fmt, biome, prettier, test-web-unit, test-unit) fails identically on clean main: run 41185521710 (main @775f9a82) failed the same 9 jobs, and its test-unit log shows the exact same 5 failures (fuel-per-call-kind, lock sqlite/logsync, sandbox fuel budgets, 2× browser-websocket Roc). The fmt/clippy failures are Magic Nix Cache throttling (HTTP 418, substituter disabled) — infra, CI: hosted runner queue saturates repo-wide (~425 queued runs), so PR checks wait hours behind push and workflow_run traffic #2605 family.
    • Codex review on current head: completed, no findings, 0 unresolved threads.

    Merging under the standing rule (checks red only on the verified pre-existing set; the change itself is verified sound). Full-tier job still queued behind repo saturation (#2605); on main it fails on the same set, so it is not a differentiator.

  8. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — done (queue task 4240): PR #3058 merged facf648; issue auto-closed. CI attribution and evidence in the merge note above; queue/backlog/sweep.log updated.

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)kind/securitySecurity issue or security hardeningpriority/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