List pages usually apply a tenant and role filter. Batch endpoints often skip it. The client sends GET /records?ids=12,45,99 or POST /records/batch { "ids": [...] }, and the server loads every matching primary key and returns the rows.
Someone who only sees their own projects can still paste a coworker's record id into the batch call and get the full object back. The UI never listed that row, so nobody noticed the hole.
Run the same read check you use on the single-record show endpoint for each id before you add it to the response. Drop ids that fail, or return 404 for the whole batch if your API treats unknown and forbidden the same. Do not leak titles in error messages for the ones you drop.
Quick test: pick a record the test user cannot open via /records/:id, put that id in a batch request with two allowed ids, and confirm the forbidden row is missing from the payload.
Top comments (1)
Seen this bite teams moving from REST to GraphQL — batch resolvers (
DataLoaderstyle) often bypass the same policy checks the list resolver enforces. The fix isn't "add checks to batch endpoint" but "factor the authorization predicate into a shared query scope / policy object both paths call". If your ORM supports global scopes (Eloquent, Prisma middleware, SQLAlchemy events), bind the tenant/role filter there so every query — single, batch, raw — inherits it automatically. Also worth auditing any admin/debug endpoints that accept ID arrays; they're the usual escape hatch — found it via LabAgent, site: labagent .tech