|
| 1 | +--- |
| 2 | +'@objectstack/plugin-security': patch |
| 3 | +'@objectstack/objectql': patch |
| 4 | +'@objectstack/plugin-sharing': patch |
| 5 | +--- |
| 6 | + |
| 7 | +fix(security): guard the `owner_id` ownership anchor and scope bulk writes to owner-visible rows (#3004, #2982) |
| 8 | + |
| 9 | +Two write-path holes on the row-ownership anchor (`owner_id`), the column OWD |
| 10 | +row-level scoping keys off to decide who may update/delete a record. |
| 11 | + |
| 12 | +- **#3004 — client-writable, unguarded `owner_id`.** The anchor is deliberately |
| 13 | + not `readonly` (ownership is transferable), so the static-readonly strip never |
| 14 | + covered it and FLS doesn't gate it by default. A non-privileged writer could |
| 15 | + therefore `insert` a record under someone else's name (forge) or `update` one |
| 16 | + to a new owner (transfer / disown), evading the owner gate that governs |
| 17 | + update/delete. The security middleware (plugin-security step 3.5) now treats |
| 18 | + `owner_id` as system-managed for non-privileged writers: on insert an empty |
| 19 | + value is auto-stamped to the acting user (batch rows too — previously only the |
| 20 | + single-record path stamped, leaving bulk-inserted rows NULL-owned and |
| 21 | + invisible to their creator), and a supplied foreign owner is denied; on update |
| 22 | + a supplied `owner_id` is a transfer/disown and is denied — the unchanged no-op |
| 23 | + echo of a form save is tolerated via a pre-image compare, and a bulk |
| 24 | + change-set carrying `owner_id` fails closed. A non-scalar `owner_id` |
| 25 | + (array/object) is rejected outright rather than string-coerced, and the |
| 26 | + change-set membership test uses own-property semantics so a polluted |
| 27 | + prototype cannot spoof an ownership write. Both require the transfer grant |
| 28 | + (`allowTransfer`, or `modifyAllRecords` which implies it) to proceed. System |
| 29 | + context (`ctx.isSystem`) stays fully exempt (OAuth provisioning / cron |
| 30 | + snapshots / seed claims / migrations), and under delegation both principals |
| 31 | + must hold the grant (ADR-0090 D10 intersection). Note a REST **import** runs |
| 32 | + under the importer's own context (not `isSystem`), so a non-privileged user |
| 33 | + importing a CSV whose `owner_id` column names other users is correctly denied |
| 34 | + unless they hold the transfer grant — administrators (who carry |
| 35 | + `modifyAllRecords`) are unaffected. |
| 36 | + |
| 37 | +- **#2982 — bulk writes skipped owner scoping on OWD-`private` objects.** A |
| 38 | + `update({ multi: true })` / bulk delete rebuilt the driver AST from |
| 39 | + `options.where` AFTER the middleware chain, discarding the owner/RLS write |
| 40 | + filter that plugin-sharing (`buildWriteFilter`) and plugin-security compose |
| 41 | + onto `opCtx.ast` — so a member's bulk write hit every matching row, including |
| 42 | + peers'. The engine now seeds `opCtx.ast` from the caller's predicate BEFORE the |
| 43 | + chain (the same seam reads use) and hands the middleware-composed AST to |
| 44 | + `driver.updateMany` / `driver.deleteMany`, so bulk writes are constrained to the |
| 45 | + rows the caller may edit — matching single-id write behavior. `delete` now |
| 46 | + applies the same scalar-`id` guard `update` already had, so an id-list bulk |
| 47 | + delete (`where: { id: { $in: […] } }, multi: true`) is owner-scoped too, and |
| 48 | + both multi branches fail CLOSED (throw) rather than silently rebuilding an |
| 49 | + unscoped predicate if the row-scoping AST is ever absent. |
| 50 | + |
| 51 | + Consequences of routing bulk writes through the AST: the anti-oracle |
| 52 | + predicate guard now also applies to bulk `update`/`delete` (a bulk write |
| 53 | + filtering on an FLS-unreadable field is rejected, as reads already are), and a |
| 54 | + principal-less (no-`userId`, non-system) bulk write on an owner-scoped object |
| 55 | + now correctly affects zero rows instead of all of them. |
| 56 | + |
| 57 | +Proven end-to-end on the real showcase app |
| 58 | +(`packages/dogfood/test/owner-anchor-and-bulk-writes.dogfood.test.ts`) and pinned |
| 59 | +in the ADR-0096 authz-conformance ledger (`ownership-anchor-guard`, |
| 60 | +`bulk-write-owner-scoping`). |
0 commit comments