Skip to content

Project and roster ids resolve through the prototype chain: project-bind {id: "__proto__"} writes Object.prototype and every unbound member then reports that Project #1704

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 818e2366ced944b0a8341b2d746f1242
Branch: dev/3-proto-safe-project-ids
Base: d534703
At: 2026-10-29T14:27:34Z

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.

Written-by: FlyBot Lead

What goes wrong

projectOfId (core/src/host/store.js:364) and projectMemberOf (:370-373) read the state tables as state.projects[id] / state.bots[id] / state.rooms[id]. Those tables are plain objects, so id: '__proto__' resolves to Object.prototype, and isRecord(Object.prototype) is true. Two consequences, both real on live main d5347032:

  1. project-read {id:'__proto__'} answers with a Project ({} on the wire) instead of {project: null}.
  2. project-bind {kind:'bot', id:'__proto__', projectId:'<a real Project>'} answers ok: true and runs member.projectId = project.id (:416-421) — i.e. it writes projectId onto Object.prototype. From then on every member without its own binding inherits it: project-of {kind:'bot', id:'<any unbound bot>'} and {kind:'group', id:'<any unbound group>'} return that Project. The binding is a phantom: state.json carries no bots.<id>.projectId own key (Object.hasOwn false), so the next restart clears it — after the API already said ok: true.

The same lookup shape sits in findBot (:707-713) and findRooms (:715-720) and predates #1695: findBot('__proto__') returns Object.prototype, whose .remote reads as undefined, so it looks like a team Bot; findRooms('__proto__') returns it as a room. (constructor/toString are safe today only because isRecord refuses functions.)

Nothing in the UI calls these endpoints yet — grep -rn "project-" core/src/client plugins/*/client.js is empty, so no user can click this today. It matters because #1701/#1702 read project-of as their data source: one such call would show every unbound Bot and group in the board under one Project, and the state only clears on restart.

Reproduction (live main d5347032)

Probe script uses the repository's own test/fake-ctx harness:

DSH_HARNESS=<dsh checkout> TMPDIR=/tmp node --import ./test/harness-resolve.mjs probe-projects-proto.mjs

Observed (exit 1):

  • project-read {id:'__proto__'} → {} — expected null
  • project-bind {kind:'bot', id:'__proto__', projectId:<real>} → ok: true — expected bot/not-found
  • project-of {kind:'bot', id:'b'} and {kind:'group', id:'r1'} (neither bound) → the Project above — expected null
  • in-process ({})['projectId'] → visible
  • state.json: bots.b has no own projectId → binding gone after restart

Expected

  • An id that is not an own key of its table resolves as absent: project-read → {project: null}, project-bind / project-of → bot/not-found / null.
  • Object.prototype is never written; a __proto__ request leaves unrelated members untouched.
  • findBot / findRooms keep today's behaviour for real ids and names and stop returning Object.prototype for __proto__.

Scope (S)

  • core/src/host/store.js: one shared lookup helper used by projectOfId, projectMemberOf, findBot and findRooms — own-key only (or null-prototype tables, if that reads cleaner; one convention for the whole file, not two).
  • One regression test in the file that owns the Project endpoints, failing on d5347032 with the symptoms above.
  • Regenerate the patches/ds-bot/files/** mirrors; node scripts/ds-bot-patches.mjs check stays green.
  • Do not touch authz.js (these endpoints stay "signed-in actor"), do not add an endpoint, do not rename fields.

Acceptance

  1. Red-first on d5347032: the new test fails with exactly the symptoms above; at the tip the whole Project suite is green.
  2. project-read / project-bind / project-of with __proto__ and with a real but unbound id: no Object.prototype write, correct not-found / null.
  3. findBot('__proto__') → undefined; findBot(<real bot name>) and findRooms(<real room name>) unchanged.
  4. TZ=UTC ARCH_DOCS_BASE=<live main> npm run verify exit 0; ds-bot-patches check green; core/ mirrors updated in the same commit.

Found by FlyBot Product's scheduled audit (P-162, baseline d5347032); line numbers above re-read by me on d5347032.

Activity

  1. added 2 commits that reference this issue on Oct 29, 2026
    2f5f638
    8c4df9b
  2. HugoXK commented on Oct 29, 2026

    @HugoXK
    ContributorAuthor

    Release

    Written-by: FlyBot Lead
    Released: 818e2366ced944b0a8341b2d746f1242
    Reason: merged as a932752 (PR #1708 at 8c4df9b)
    At: 2026-10-29T16:09:27Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions