All resources
// Resources

Testing Broken Object-Level Authorization in APIs

Published June 10, 2026

Ownership decision at data boundary

BOLA review starts where a record is selected. Write a two-tenant fixture with equal roles, then enumerate identifiers in path, query string, request body, cursor, relationship field, and asynchronous job payload. For example, an export request may correctly reject /reports/beta yet accept { "reportId": "beta" } in a bulk body if repository filtering is missing. Test read, update, delete, download, and job-status calls independently.

Expected output is not only 403 or 404. Capture row count, file generation, processor call, emitted event, and cache behavior. A 404 is acceptable when disclosure policy chooses it; it is a failure if a response-size difference, generated file, or status endpoint reveals tenant B data. Queue workers must carry trusted tenant and subject context, and verify it again before processing rather than trust an old serialized request.

Failure closure means changing lookup so tenant predicate and object identity are inseparable, or calling a shared policy before lookup. Test owner records foreign-ID request, expected denial, observed state, patch reference, and retest with fresh sessions. An exception such as a delegated accountant needs documented delegation relationship and expiry; it must not become a broad cross-tenant role.

Use one fixture object per tenant and avoid guessing production identifiers. A foreign UUID test must cover response cache and download URL, because a correct primary lookup can be followed by weak secondary fetch. If an endpoint accepts arrays, mix own and foreign IDs and define whether valid items proceed or entire request fails. Evidence should prove chosen behavior. After code change, inspect generated SQL or repository predicate to confirm tenant filter remains attached on every branch.

Object access closure

Build fixtures where tenant red and tenant blue own otherwise identical invoices, files, report jobs, and child objects. Submit foreign identifiers in path, body, query, nested relationship, cursor, and bulk arrays. Check list totals and pagination cursor as carefully as item payload; count or timing can disclose record existence. A download route must reauthorize when generating signed URL rather than inherit a prior list decision.

For a failing foreign-ID request, observe database row count, object storage lookup, queue enqueue, cache key, and audit entry. Expected result may be 403 or 404 by disclosure policy, but it must have no foreign content and no downstream effect. Worker payload should hold stable authorization context and recheck membership when job executes, since delegation can change between enqueue and run.

Repair object access by binding tenant predicate to lookup or by calling policy with trusted subject before load. Retest using a newly issued session and a cached client path. Delegated access is acceptable only when relationship has owner, scope, and expiry; broad “support” membership is not substitute for object policy.

Additional mechanism checks

Object graphs create indirect references. A user can be authorized for an order yet not for every attachment, payment method, or child job reachable from it. Test parent substitution and child substitution separately. Cursor encodings must be treated as opaque input, not proof caller previously saw a collection. Cache keys must include authorization-relevant tenant or subject dimensions; otherwise an allowed fetch can seed a foreign response. For pre-signed downloads, use short expiry and reauthorize at issuance; do not expose bucket key merely because an earlier page displayed file name.

Repository and worker checks

Review code where object is loaded, not only controller middleware. A repository query should combine tenant and object predicate, for example selecting invoice by both invoice_id and trusted tenant context. A policy layer can decide first, but later lazy loads and file lookups must retain same context. Instrument test environment to record storage read and queue enqueue; a 404 that still touches foreign export object is not clean.

Adjacent negatives matter. After foreign invoice denial, try foreign attachment ID under an owned invoice, foreign cursor after a valid page, and polling a job created by another tenant. Responses must reveal neither file URL nor job progress. Then verify own object remains available after cache invalidation. This distinguishes deliberate tenancy enforcement from an accidental cache miss.

Delegation is an explicit object relationship. Test its start, expiry, and revocation. A delegated accountant may read stated ledger for stated tenant, yet cannot inherit payment-method or account-administration access. Evidence records relationship ID, policy decision, expiry time, and post-revocation request.

Foreign-object test ledger

Object substitutionOwnerEvidenceExpected resultObserved result / statusExceptionClosure
Tenant Red requests Tenant Blue invoice inv-blue-02 through POST /exports bodyData authorization ownerCapture BOLA-118; SQL trace includes tenant_id=red; storage and queue counters404 with no foreign row, file, job, or cache entry404; rows 0; storage reads 0; jobs 0 — closedNoneFresh-session and mixed-bulk retests passed; patch SEC-381 accepted 2026-06-10

Sources

Have a system that needs testing?