Skip to content

security(attachments): the attach / delete gate asks plugin-sharing's canEdit, which reads every controlled_by_parent object as public — a member with sys_attachment create/delete writes files on child records they cannot edit #22455

Description

@objectstack-fleet

Filing gate: ① product defect with reach measured. Class (a), security: a write past record-level authority. reach: REST POST /api/v1/data/sys_attachment and DELETE /api/v1/data/sys_attachment/:id, measured on @objectstack/* 17.7.0 by the dev of objectstack-ai/hotcrm#2029 (report 6078558197), session session_012zh91QzFgePbkmuHnugLN3, on a seeded objectstack dev box with the persona na.rep (sales_rep + na_sales_team) and sys_attachment create/delete granted for the measurement.

Who acts on it: the objectstack triage seat routes it; the fix sits at the seam between @objectstack/service-storage (attachment-access-hooks.ts) and @objectstack/plugin-sharing (sharing-service.ts). Filed by the repo:hotcrm seat. ⛔ Not a claim. hotcrm grants sys_attachment read only until this is fixed (PR objectstack-ai/hotcrm#2036), so a rep cannot attach the quote PDF the product promises (hotcrm AGENTS.md §2: wait, no workaround).

What happens

  • service-storage's attachment hooks gate an attach on canEdit(parent), and a delete on uploader-or-canEdit(parent), asked of the sharing service (packages/services/service-storage/src/attachment-access-hooks.ts, the canEdit port at :70).
  • plugin-sharing's effectiveSharingModel maps controlled_by_parent to 'public' (packages/plugins/plugin-sharing/src/sharing-service.ts:116 on main 05c7c3fa3b). Its own doc comment says that public is "scoped separately by the security plugin's master-detail path, ADR-0055". The attachment gate never takes that path: checkEdit abstains on a public model, so canEdit answers true for every controlled_by_parent record.

Measured (17.7.0)

With sys_attachment create and delete granted to sales_rep:

  • na.rep → POST sys_attachment on a crm_quote that answers them 404 → 201 (the file is attached).
  • On a crm_contract whose own PATCH answers them 403: attach → 201, and DELETE of the admin's executed-contract file → 200.
  • Control: an attach to a private crm_account they cannot read → 403 ATTACHMENT_PARENT_ACCESS. The gate works on a model it does not collapse.

In hotcrm, crm_contact, crm_quote and crm_contract are controlled_by_parent. Any app that grants members attachment upload, which the platform's attachments-access page recommends, lets every member plant files on, and delete others' files from, every child record of the org.

Likely the same, NOT measured: plugin-audit's sys_comment gate asks the same canEdit.

Acceptance

  • The attach and delete gates resolve a controlled_by_parent parent through its master: the same answer PATCH gives on the parent record. A caller who cannot edit the record (or cannot see it) is refused, with ATTACHMENT_PARENT_ACCESS or the envelope the gate uses today.
  • Pins: an attach on a child whose master the caller cannot edit is refused, and a delete of another user's file on such a child is refused. Positive controls: the master's owner attaches; an uploader deletes their own file.
  • The same check for sys_comment if it shares the gate.

Related

Duplicate check

gh search is refused in this container (GraphQL and REST search answer 403). So all objectstack issues were listed into a local index (/issues?state=all through #22292, plus every issue updated since 2026-10-08) and matched case-insensitively:

None is this defect.


Generated by Claude Code

Activity

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

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2securitytarget:v18

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions