Keep the query boundary on every page
Use a fixture that places A and B records next to each other in sort order. Ask for a small page size so allowed rows span several pages while every foreign row stays out. Check total counts, cursors, and exported row totals. Change the filter after page one; the old cursor should not silently continue under the new filter. For exports, let the job finish, revoke the requester’s access, then test the download. If the product promises immediate revocation, use a checked download route and test any already-issued bearer link separately.
Test cases and proof
| Case | Expected result | Proof to keep |
|---|---|---|
| Use another user’s cursor | Reject or scope to caller | Page record IDs |
| Change access after export starts | Recheck access at download | Download result |
| Ask for all rows through page size | Enforce bounded page size | Response count and server work |
Synthetic example
User A opens page one, then gives the next-page cursor to user B. If the cursor encodes only an offset, B may receive A records. Bind the cursor to filters and identity or reapply access checks to every result. Run the same check on exported files after the job finishes.
Evidence to keep
Save the cursor issued to each user and the exact filters that produced it. Record the returned record IDs for every page, not a screenshot of page one. For exports, keep the job creator, time of role change, file owner, and download attempt. These facts show whether the leak came from cursor replay, a missing row filter, or a result file that outlived access.
Two places to close access
A page cursor can be bound to a user and filter, but the server still needs a row-level check when it builds each page. An export can finish under a worker account with broad rights; the download endpoint must decide whether the human requester still has access. If a role changes while the job runs, the team must choose whether to stop work or only block download. Write that rule down before testing, then prove it with the job log and a denied file request.
Related reading
Why these checks matter
OWASP’s API risks cover object-level authorization. Its authorization guide says checks belong on every request. A cursor or completed export is still a request for records, even if the first page or job creation was allowed. The test therefore checks each page and the final download after access changes.
Check a page that contains only allowed rows
Synthetic data: A owns A1 through A5; B owns B1 through B5. Their created_at values alternate, with a unique ID as the second sort key. Fetch as A with limit=2. A’s first page must contain two A IDs and a usable cursor; continue until all five A IDs appear exactly once. The alternating B rows test filtering, not an expectation that both tenants appear. Repeat as B, then swap the cursors.
Replay may be rejected, or it may produce only rows allowed to the current caller under a documented cursor contract. Either outcome can be safe. The failure is a foreign row, foreign total, or weakened filter. Include equal timestamps to expose unstable sort order, and change the filter to check whether the cursor is rejected or safely interpreted under the new scope.
Choose a revocable export path
A direct presigned storage link is a bearer grant. Possession can permit download until expiry even after app permissions change. It cannot perform a new customer access check by itself. If immediate revocation is required, use an authenticated result route that checks current rights and serves or proxies the file. Redirecting to a fresh long-lived bearer link still creates a period of access.
Create an export, revoke A before download, and test the app route plus any already-issued storage link separately. Record whether the system promises immediate denial or a bounded link lifetime. Previously downloaded files are outside server revocation. Keep job creator, query hash, row IDs, permission decision, file hash, and link expiry without storing the secret URL in the report. AWS documents bearer-link access.