Skip to content

Gate reference-creation on file/read access in ScopedStack (#51)#98

Merged
cuibonobo merged 1 commit into
mainfrom
claude/issue-51-design-review
Jul 16, 2026
Merged

Gate reference-creation on file/read access in ScopedStack (#51)#98
cuibonobo merged 1 commit into
mainfrom
claude/issue-51-design-review

Conversation

@cuibonobo

Copy link
Copy Markdown
Member

Summary

ScopedStack.create() forced entityId to the requester but forwarded parentId, associations, and permissions-adjacent options straight through unchecked. The sharpest hole: attachment access is reference-implies-access (getAttachment() succeeds if the requester can read any record referencing a fileId), but nothing gated creating such a reference — a requester holding only a bare create grant could attach a private (or guessed-hash) file's fileId to their own record and read it back, including using success/failure as a confirmation oracle for guessed content hashes.

  • canAccessFile(fileId) — the dual of getAttachment()'s access rule: owner, uploader, or already-readable-referencer. getAttachment() now calls this instead of duplicating the logic.
  • canReadReferent(recordId) — read access to a parentId or relationship association target.
  • Both create() and associate() now gate attachment associations and file-ref content fields on canAccessFile, and relationship associations / parentId on canReadReferent. tag associations carry no reference and are never gated.
  • A missing target and an existing-but-forbidden one always throw the same generic StackPermissionError — indistinguishable, so the check itself can't leak existence.
  • _group roster associations (member/admin relationship links) are exempt from the relationship-target check: their recordId names an entity, not a readable record, and roster mutation is already gated far more strictly by Group roles are decorative: admin should mean "can manage the group" #58's admin-or-owner rule.
  • Extended beyond the original issue text to also cover file-ref content fields (create()/update()): file-ref field kind: content-held file references are invisible to the reference checks the spec says exist #63 landed after ScopedStack.create() passes caller options through unchecked — attachment-association escalation, parent/app spoofing #51 was filed and made a file-ref field convey attachment access identically to an attachment association — same vulnerability class, same fix.
  • permissions and appId are deliberately left ungated at create time, per the issue's own resolution: permissions is already owner/creator territory via setPermissions(), and appId is self-reported, untrusted metadata everywhere, not a permission input. Both documented in the spec rather than code-gated.
  • docs/spec.md updated: new "Reference-creation gating" paragraph under Permissions/Enforcement, a pointer from the Associations section, and the appId/permissions policy statements.

Depends on #50 (uploader-check pagination fix — already merged).

Test plan

  • packages/core typecheck clean
  • packages/core lint clean
  • Full packages/core test suite passing (408/408), including new coverage:
    • attachment-association gating on create(): rejects inaccessible files, allows uploader/owner/already-readable-referencer, tag associations ungated, owner exempt
    • anti-oracle: nonexistent vs. existing-but-forbidden fileId produce byte-identical errors
    • relationship-association and parentId gating on create(): same allow/reject/anti-oracle/owner-exempt coverage
    • _group roster associations remain exempt from the relationship-target check
    • associate() gating for attachment and relationship associations on an already-owned record
    • file-ref content field gating on create() and update(), including the untouched-field-in-a-patch carve-out

Generated by Claude Code

#51)

ScopedStack.create() forwarded parentId, associations, and file-ref content
fields from the caller unchecked, so a bare create grant let a requester
attach a private file (and read it back through the reference) or plant
relationship/parentId links to records they can't read. associate() had the
same hole for attachment associations. Both now require what possessing the
reference would grant: file access for attachment associations and file-ref
fields, read access to the target for relationship associations and
parentId. tag associations and _group roster associations (gated more
strictly by #58's admin check) are exempt. A missing target and an
existing-but-forbidden one always produce the same StackPermissionError, so
the check can't become an existence oracle.

Extends the fix to file-ref content fields (#63) since they convey
attachment access identically to attachment associations — the same
vulnerability class #51 describes, just added after #51 was filed.

getAttachment() now shares its access predicate with the new gate instead
of duplicating the logic.

permissions and appId are intentionally left ungated at create time, per
the issue's resolution: permissions is already owner/creator territory via
setPermissions(), and appId is self-reported metadata everywhere, not a
permission input.

Refs #58 (_group roster exemption), #63 (file-ref reference parity), #50
(uploader-check pagination this depends on, already landed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDmUW3oNxPSCLf2ftqaF8q
@cuibonobo
cuibonobo merged commit 855697f into main Jul 16, 2026
5 checks passed
@cuibonobo
cuibonobo deleted the claude/issue-51-design-review branch July 16, 2026 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants