Skip to content

id(artifact): home_declared world_id gets only an is_empty check in parsed() — a 100 KB, whitespace-only, control- or bidi-bearing world_id is accepted and stored, and the same endpoint can be listed twice with different worlds #3187

Closed

Description

@dezren39

Problem

SignedArtifact::parsed() (pkgs/id/src/artifact.rs) validates every field of every known kind against an explicit bound — except HomeServer.world_id in a home_declared body. The HOME_DECLARED arm (artifact.rs:334-355) checks the account key, that the author is the account, that there are at most MAX_HOME_SERVERS (8) servers, and that each endpoint parses via check_target (an endpoint ticket or a bare node ID — a tightly bounded format). For world_id it checks only !is_empty() (artifact.rs:350-352).

So a signed home_declared artifact carrying a world_id that is:

  • 100,000 bytes long,
  • whitespace-only (" " — non-empty, so it passes),
  • bearing control characters or a newline,
  • or bearing a bidi format character (U+202E),

is accepted by parsed(), stored in the author's artifact chain, and returned verbatim by ArtifactLog::declared() (artifact.rs:519-525) to every consumer. Duplicate servers are accepted too: the same endpoint listed twice, even with two different world_ids, passes — there is no duplicate check.

Every sibling field has a named bound: Document.media_type 1–128 bytes and content ≤ MAX_DOCUMENT_BYTES (artifact.rs:358-368), Tag.name 1–64 bytes and Tag.value ≤ 256 bytes (artifact.rs:376-380), FriendRemove keys checked as canonical account keys (artifact.rs:322-324). world_id is the odd one out in this arm. (The overall 1 MiB frame cap in ArtifactLog::store bounds the total artifact, not this field: a 100 KB world_id fits comfortably inside one frame.)

Evidence (verified this run, main 99ae033)

  • Code read: pkgs/id/src/artifact.rs:346-353 — the only world_id check is if server.world_id.is_empty(). pkgs/id/src/artifact.rs:150-156 — HomeServer { endpoint: String, world_id: String }, no other validation anywhere (grep world_id across pkgs/id/src: no consumer sanitises it either).
  • Laptop probe (probe-2026-10-31-laneA-homeworld/ on the laptop under ~/git/fleet/, path-dep on pkgs/id, artifact.rs sha256 bdc11bbe… identical on both clones) driving the real SignedArtifact::sign + parsed():
    • control "home" → ACCEPTED
    • "" → REFUSED (the one existing check)
    • "home\nFORGED" → ACCEPTED
    • control characters (\u{1}, \u{7f}) → ACCEPTED
    • U+202E bidi → ACCEPTED
    • " " whitespace-only → ACCEPTED
    • 100,000-byte world_id → ACCEPTED
    • same endpoint three times (twice with different world_ids) → ACCEPTED, 3 servers

Why it matters

A home declaration is how an account tells the network where its artifacts live; declared() feeds directory views and home-server selection. An unbounded, unsanitised world_id is stored and propagated exactly as signed — oversized values bloat every chain page that carries the declaration, and control/bidi-bearing values reach any present or future rendering of the declaration unchecked, the same stored-payload class the neighbouring fields' bounds exist to prevent. The duplicate-endpoint case makes the declaration ambiguous: two entries claim the same endpoint holds the account in two different worlds, and nothing downstream disambiguates.

Acceptance criteria

  • parsed() refuses a home_declared whose any world_id exceeds a named bound (in line with the sibling fields, e.g. 64 bytes), is whitespace-only, or contains control/format characters — with tests pinning each refusal at the parsed() level.
  • parsed() refuses a declaration that names the same endpoint twice (or the triage pins, with a test, why duplicates are harmless and which entry wins).
  • Ordinary declarations (the existing tests' "home" / "lobby" world IDs, ≤ 8 distinct endpoints) parse exactly as today.
  • The bound is named alongside the existing constants in artifact.rs (MAX_HOME_SERVERS, MAX_DOCUMENT_BYTES, …), not a magic number at the check site.

Triage note: sibling gap, not a duplicate — #3146 covers friend_remove.requests in the same function; this is the home_declared arm's field. Whether the fix also belongs in the design contract of #3068 (parsed-shape contract) is for triage to link.

Activity

  1. 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/lowNice to have; do when convenient
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    effort/sSmall: expected under 2 hours of work
    on Oct 31, 2026
  2. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-10-31 13:35 CDT
    • Found via: filed this run
    • Task: Filed this issue — home_declared's world_id is the least-checked field in SignedArtifact::parsed() (artifact.rs:350-352, only is_empty); verified on main 99ae033 with a laptop probe (probe-2026-10-31-laneA-homeworld, real sign+parsed(), artifact.rs sha256 bdc11bbe on both clones): newline/control/U+202E/whitespace-only and a 100,000-byte world_id all ACCEPTED, empty REFUSED, same endpoint listed 3× ACCEPTED. Sibling of id(artifact): friend_remove requests is the one known-kind body parsed() never validates — any count, any shape, any length passes, and every string lands permanently in the directory's removed_requests void guard #3146 (friend_remove arm), distinct field. Enqueued as a repo-task/v1 packet on repo-health-code-pilot.
    • Expectation: effort/s — add a named world_id bound + control/format refusal and a duplicate-endpoint rule in parsed(), pinned by parsed()-level tests; ordinary declarations unchanged.
    • Links: packet packet-3187-triage.json (hidden_files, keep-the-four-repos-healthy goal)
  3. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Design-architect pass 28 (2026-10-31) — agent note: this issue is ADOPTED as a phase of design #3231 ("a signed artifact is validated as strictly as it is built", design doc: files/designs/code-id-artifact-receipt-validation.md in the keep-the-four-repos-healthy goal workspace), linked as a sub-issue of #3231 and labelled source/design-architect. Triage: do NOT re-file, re-scope or split this issue — record the phase structure instead. Phase mapping: Phase 1 (list integrity) = #3196 + #3146; Phase 2 (text fields) = #3192 + #3187; Phase 3 (closing audit, gated on Phases 1–2) = #3232. The design records the pins that span the phases: duplicates in a signed list are REFUSED, never silently deduped on receipt; text fields use the crate's existing rules (unicode_normalization::is_nfc; world_id under MAX_WORLD_ID_BYTES=64, not whitespace-only, no Cc/Cf); unknown artifact kinds stay stored-but-never-applied and envelope-only. Each origin keeps its own acceptance criteria and its already-queued triage packet — no new packet was created for it this pass.

  4. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  5. 166 remaining items

  6. 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 4, 2026
  7. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  8. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  9. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 4, 2026
  10. added a commit that references this issue on Nov 4, 2026
  11. removed
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    work/assignedQueue worker lifecycle state
    on Nov 4, 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/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions