Skip to content

Audit Tenant Filters on List APIs

Trace list queries and test that each response contains only records the caller may see.

List APIs can leak more than a record detail page. A handler may check that a user belongs to some tenant, then run an unscoped query and return rows from every tenant. Pagination totals and facets can leak information even when visible rows are filtered later. OWASP's multi-tenant guidance calls for isolation at each data access path.

Inventory the list path

For every list endpoint, record the route, caller identity source, query builder, tenant predicate, pagination method, count query, and serializer. Include administrative and background variants. A list owned by a project may need both project membership and tenant ownership; a tenant predicate by itself is not always enough.

Response partBoundary questionEvidence to capture
RowsIs each row in the caller's permitted set?Synthetic tenant markers
Total countDoes the count use the same scope as rows?Expected and actual totals
FacetsAre categories computed only from permitted rows?Facet values by tenant
PaginationCan a cursor cross tenant context?Cursor reuse denial
Empty stateDoes it reveal another tenant's existence?Response comparison

Use Tenant A and Tenant B with distinct synthetic records. Test an A member against the list route, then repeat with B and an account that belongs to both. A valid multi-tenant member may see a union only when the product policy permits it. Also test a newly revoked member and a filter value that matches only another tenant's record.

Apply the scope before counts, sorting, and pagination. Filtering after a database query can still expose counts, consume another tenant's records in a page, or create unstable page boundaries. A response-level filter is not a substitute for a trustworthy query constraint.

Check counts and facets

Illustrative SQL for a list filtered to a verified tenant context uses the same predicate in each query:

SELECT id, category FROM records WHERE tenant_id = $1 AND category = $2 ORDER BY id LIMIT $3;
SELECT count(*) FROM records WHERE tenant_id = $1 AND category = $2;
SELECT category, count(*) FROM records WHERE tenant_id = $1 GROUP BY category;

The facet query omits the selected category filter only if the product deliberately shows all categories within that tenant. Test with a B-only category: A must receive zero matching rows and count, and no B facet. Do not take $1 from an unverified request header. PostgreSQL describes WHERE and GROUP BY processing.

Keep the test result tied to a build and route version. The OWASP API Security Top 10 treats object authorization as an API risk, but this worksheet is narrower: it checks collection behavior. If one of the five response parts has no clear owner, mark it pending. Do not call a list endpoint isolated merely because the first page looks right.

Find the gaps in your access controls

See how an illustrative cross-workspace finding connects evidence, impact and a fix. Discuss the tenant boundaries your team needs reviewed.

Sources

  1. OWASP Multi Tenant Security Cheat SheetAccessed
  2. OWASP API Security Top 10 (2023)Accessed