Broken Object-Level Authorization (BOLA) is #1 on the OWASP API Top 10 for a reason. It's pervasive, it bypasses every WAF, and it's invisible to most scanners. In modern microservice architectures it's worse than ever — because the question "who is allowed to touch object X?" often has no clear owner.
The shape of the bug
Classic BOLA: GET /v1/invoices/77a2 returns invoice 77a2 regardless of who you're authenticated as. Fix: check ownership.
Modern microservice BOLA is rarely this obvious. The anti-pattern looks like this:
- Edge service authenticates the user, resolves their org.
- Edge service calls
invoice-servicewith a header:x-user-id: alice, x-org-id: 77a2. invoice-service*trusts the headers*. It filters byx-org-id.- Somewhere, someone can call
invoice-servicedirectly and set whatever headers they want.
Direct-access BOLA is common in:
- Internal service meshes that forgot to enforce auth on the east-west path.
- Mobile apps that talk to multiple backends, one of which trusts the JWT claims instead of re-validating.
- Legacy endpoints (the
/internal/or/debug/variety) that were supposed to be stripped in production. - "Batch" endpoints that take a list of IDs and loop — where the per-item authorization check got forgotten.
How we find it
Our playbook on a new customer looks like this:
- Asset graph first. We enumerate every route and every backend service. Anything that responds to a request and isn't on the graph gets flagged.
- Per-endpoint authorization fuzzing. For every
GET/POST/PUT/DELETEthat takes an ID-shaped parameter, we generate a second test: call the same endpoint with a second-tenant auth, victim-tenant ID, in every variant (path, query, header, body, JWT claim). - Direct-service probe. If the service mesh permits it, we bypass the edge and call backend services directly with forged headers. You'd be surprised how often this works.
- GraphQL selection-set abuse. GraphQL moves authorization from the endpoint to the resolver. Resolvers get written by ten different engineers. Not all of them check auth.
- Batch mode.
POST /orders:batchwith{"ids": ["mine", "yours"]}— does the service loop correctly?
A real chain
In a recent engagement we found a BOLA that looked like this:
- Public API enforced per-org ACLs correctly.
- A webhook-delivery service accepted internal messages that included a
target_urland apayload. - Because the webhook service was "internal" and "async", nobody treated its input as attacker-controlled.
- But: a separate, public feature accepted a user-provided URL that went through the webhook service.
Net result: from a regular user account we could pivot through the webhook service to hit internal URLs (SSRF), some of which returned payloads including cross-tenant data (BOLA-by-pivot).
What to test this quarter
- List every service. For each: authentication, authorization, and who trusts who?
- Find your "internal" services. Probe them from the edge.
- For every endpoint with an ID parameter, prove authorization from code.
- Stop trusting headers between services unless you've signed them. mTLS or SPIFFE minimum.
- Write contract tests. "Cross-tenant GET must 403" is a test, not a principle.
BOLA doesn't need a zero-day. It needs a team that treats authorization as a first-class concern instead of the edge-service's problem.
