A batch endpoint must authorize every item, not just the first
Bulk endpoints are handy: archive 50 documents, delete 20 comments, move 100 files in one call. They are also a quiet place for authorization to slip.
The trap
The handler receives ids: [...], loads the first record, checks that the caller can edit it, then runs UPDATE documents SET archived = true WHERE id = ANY(:ids).
That proves the caller may touch one document. It says nothing about the other 49, which might belong to another project, another team, or another tenant.
Why it happens
- The single-item route had a check, and the bulk route reused only half of it.
- Checking every id feels slow, so someone "samples" the first one.
- The UI only shows the user's own items, so nobody expects a mixed list.
- A direct API call can send any ids it likes, including ones copied from another account.
A safer shape
- Load all requested records in one query.
- Authorize the caller on each record (or filter the query by what the caller may access).
- If any id is missing or denied, decide on purpose: reject the whole batch, or apply only the allowed ones and return which were skipped.
- Run the write only on the ids that passed, inside the same transaction.
Tiny checklist
- Can I send a sibling tenant's id in the middle of a batch and have it change?
- Does the bulk route use the same policy check as the single-item route?
- Do partial failures get reported, instead of silently succeeding?
- Is there a sane max batch size so the per-item check stays cheap?
Batch for speed, but authorize per object. The database will happily update every row you hand it.
