Guide 125 checklist

How to review pagination for information leaks

Review pagination for information leaks through cursor context, filter changes, current authorization, count accuracy, and misleading empty or incomplete results.

Review a paginated endpoint as an authorization decision over a whole collection, not just a check on each visible page. First establish the set of records and fields this caller may see. Then inspect how filters, sort, counts, facets, page size and continuation tokens operate on that set. A page token is a pointer to where to continue; it must not grant access by itself. Check that a later page re-evaluates current permissions and cannot reveal records excluded from page one, including through totals or cursor behavior. If the policy engine returns only a partial authorized set or the cursor's trust model is unknown, leave the result unresolved rather than assuming an empty page is safe. OWASP's Authorization Decisions and Output Handling guidance explicitly applies restrictions to lists, counts, exports and other outputs.

Use one small, synthetic collection

Illustrative records only; no real account data was accessed. In Finch Projects, a developer can read two assigned findings, A and D, from a six-finding account queue. Findings B, C, E and F are outside that developer's current assignment. The endpoint GET /findings?page_size=1 returns one item, a total count, a severity facet and a next_page_token. The developer asks whether pagination can expose hidden findings even if each returned row is filtered.

The desired result is specific: page one and any continuation may return only A and D, with fields that role may see. If totals are included, they must describe the permitted set, so the visible total is 2, not 6. A severity facet must count only permitted records. The token may reveal neither a hidden finding ID nor another account's cursor state, and following it after assignment revocation must not reproduce yesterday's authorized page. These numbers are invented to make the decision checkable, not a statement that every API must return exact totals.

The Google AIP-158 pagination design is useful here because it says page tokens should be opaque and should not serve as authorization to the underlying resources. It also expects other request arguments to remain the same across pages. This is Google API design guidance, not a universal security standard; the security requirement comes from the application's access policy and OWASP's per-request authorization guidance.

Fill the pagination review checklist

Check Finch question Evidence or expected result
Authorized collection Is the account, repository and assignment restriction applied before pagination? Query predicate or bounded authorized-ID set reflects A,D only.
Fields Can a visible row contain a restricted title, note or nested object? Response projection is role-specific on every page.
Filters and sort Can changing severity, repository or order select hidden rows? All combinations operate within the same authorized set.
Page size and offset Can oversized or negative values change the query or bypass limits? Server bounds inputs and preserves authorization.
Cursor integrity Does a token encode or expose hidden IDs, account IDs or a trusted filter? Token is opaque and validated as continuation state, not a grant.
Cursor binding Can a token from another account, role, filter or sort be reused? Reject or restart safely; never broaden the result.
Revocation What happens after assignment or membership changes? New request enforces current policy, including with an old token.
Aggregates Are total, facets, has_more and error differences computed over permitted data? No excluded finding metadata is disclosed.
Other surfaces Do export, search and direct links apply the same restrictions? Independent delivery paths have their own current checks.

The checklist is the article's decision artifact. Copy it into a review issue and attach the actual query, cursor construction and response-shaping locations for the pinned revision. A row count alone cannot establish what happened before filtering. If the database paginates all six rows and the application removes unauthorized results afterward, the user may see an empty page, an incorrect total, an extra continuation or a changing response pattern. Filtering after pagination also risks skipping authorized items. Prefer a query or authorization-filter plan that constrains the collection first, or a bounded method whose incompleteness is handled explicitly. OWASP's output-handling guidance warns against treating a truncated authorization result as a complete list or interpreting an empty result as permission to remove a restriction.

Read the faulty responses backward into the query

Make the existing six records concrete: A and D are assigned to the developer; B, C, E and F are not. A is medium, D is high, and all four hidden records are critical. The order is A through F. These are synthetic response examples, not requests performed against a service.

The faulty implementation takes one account row and then filters it by assignment. It calculates aggregates before that filter:

page 1: items=[A], total=6, facets={critical:4, high:1, medium:1}, next=t1
page 2: items=[],  total=6, facets={critical:4, high:1, medium:1}, next=t2
page 3: items=[],  total=6, facets={critical:4, high:1, medium:1}, next=t3
page 4: items=[D], total=6, facets={critical:4, high:1, medium:1}, next=t4

The protected rows themselves are absent. The aggregate still discloses four critical findings outside this caller's policy. The empty pages also expose a pagination contract problem: a client that interprets an empty page as completion will miss D. An empty page alone is not proof of a leak; AIP-158 explicitly permits fewer results, including zero, before the end. Here the unauthorized aggregate is the clear disclosure, and the implementation/client mismatch is a separate correctness defect.

For this small collection, the corrected logical plan is:

verify current caller and selected account
form permitted set using current assignment predicate
apply requested filter; compute permitted aggregate
apply stable sort and validated continuation; take page_size
project fields allowed for this role

The expected stable-collection responses are items=[A], total=2, facets={high:1,medium:1}, next=tA, followed by items=[D], total=2, ... with no continuation. The account total of six never enters the response. A real storage plan must enforce equivalent semantics; a policy service's first page of authorized IDs is not necessarily the complete permitted set.

Change access between the two pages

After A is returned, revoke the developer's assignment to D. Finch chooses current-policy pagination rather than promising a frozen authorization snapshot. Following tA now yields an empty final page and a current permitted total of one. That is a legitimate collection change. It must not return D simply to make the original total of two remain true.

The continuation can preserve sort position and filters while access is re-evaluated. Bind it to the appropriate request context; reject mismatched account/filter/sort use under the API's documented contract. AIP-158 allows page size to change between calls, so do not describe every changed argument as forbidden. Opacity or a signature does not turn the token into current authorization. A signed or base64-encoded payload can still reveal its contents; JWS, for example, separates signature protection from encryption. Keep hidden record IDs out of client-visible cursor data, or use a protected server-side reference or appropriately confidential representation; verify integrity and request binding separately.

The review record now separates three outcomes:

Observation Classification Required correction
critical:4 despite no permitted critical findings Unauthorized aggregate under Finch's stated policy Compute facets from permitted records
Client stops on first empty page and misses D Pagination completeness defect Honor continuation and choose a bounded, documented query plan
D disappears after revocation Expected current-policy behavior Explain changing totals; never retain stale permission for consistency

A reviewer can use this six-record fixture to inspect response semantics without estimating the behavior of a production-sized collection. Scalability remains a separate check: if the authorization lookup truncates or times out, fail or expose documented incompleteness instead of dropping the predicate.

Know what would disprove a leak

For Finch, a correctly scoped database predicate, current assignment check on every request, role-specific serialization and aggregate computation over the permitted set would defeat the proposed leak for that route. A signed cursor alone would not: signing prevents tampering but does not prove the holder is still entitled. Conversely, a total=6 response is evidence of metadata exposure even if item rows contain only A and D; the business owner must decide whether the account-level count is authorized for this role.

Record uncertainty precisely. If the authorization service may return a truncated set, the review cannot claim the visible total is complete. If the route uses a search index updated after revocation, investigate the freshness boundary. If exports use another query, this list review does not cover them. A source trace supports a hypothesis; a confirmed finding needs independent review of the deployed path and real effect. A missing alert or a single safe first page does not establish collection safety.

Review a filter change as a new collection request

Finch's developer may change severity from all findings to high findings after receiving tA. The server should apply the documented cursor contract rather than treating the old continuation as permission to keep its earlier scope. It may reject the mismatched token and require a new first page. Whatever behavior it chooses, the new filter operates only within the current authorized set.

This matters when a cursor stores a query plan or server-side reference. That stored plan can help continue ordering, but it should not freeze old permissions unless the application has explicitly designed and justified that authority model. Finch's chosen current-policy design requires a fresh authorization decision on each page. A signed old plan cannot override D's revoked assignment.

For a real review, inspect which fields are bound into the continuation and how mismatches are handled. Account, principal or authorization context, filter, and sort can affect the meaning of the token. The exact representation depends on the service; no one cache key or token format is universally correct. The important evidence is that a token cannot broaden the collection or reveal hidden continuation data.

Distinguish complete totals from useful partial results

Some storage or policy systems cannot cheaply compute an exact permitted total. The API can omit the total or expose a clearly defined partial measure instead of inventing completeness. The label should tell the client what was counted. “Two returned so far” is different from “two authorized findings exist.”

If authorization lookup times out after finding A, returning total1 as the complete backlog would mislead the developer about D. A bounded partial response may be acceptable under a documented contract, but it needs an explicit continuation or incomplete state. An empty authorization result must not cause the query to drop the restriction and list the whole account.

The acceptance record should therefore inspect both confidentiality and completeness. It asks whether forbidden rows or aggregates appeared, whether permitted rows can be reached under the documented pagination rules, and whether missing policy evidence was represented honestly. A route can avoid disclosing foreign data while still presenting a misleading total; those are different defects with different repairs.

Finch's six-record fixture keeps these questions small enough to reason through. Larger production collections need a separate performance and consistency review, but the same response contract should remain understandable to the caller.

Start by comparing one actual endpoint's query order with the checklist, using synthetic permitted and excluded records. Then inspect the first continuation and the response's total and facets. For the broader actor/action/object policy behind that set, read API authorization beyond login checks. For limits on large page sizes and costly collection work, use expensive-operation rate limits.