PentStark
Blog · AppSec

BOLA across microservices: when authorization is someone else's job

PentStark PTaaSMarch 11, 20268 min readMore on AppSec

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:

  1. Edge service authenticates the user, resolves their org.
  2. Edge service calls invoice-service with a header: x-user-id: alice, x-org-id: 77a2.
  3. invoice-service *trusts the headers*. It filters by x-org-id.
  4. Somewhere, someone can call invoice-service directly 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:

  1. 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.
  2. Per-endpoint authorization fuzzing. For every GET/POST/PUT/DELETE that 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).
  3. 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.
  4. 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.
  5. Batch mode. POST /orders:batch with {"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_url and a payload.
  • 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

  1. List every service. For each: authentication, authorization, and who trusts who?
  2. Find your "internal" services. Probe them from the edge.
  3. For every endpoint with an ID parameter, prove authorization from code.
  4. Stop trusting headers between services unless you've signed them. mTLS or SPIFFE minimum.
  5. 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.

Get research like this monthly.

No marketing fluff. Unsubscribe anytime.

Talk to an operator

Your next finding is one scoping call away.

Thirty minutes with a real operator tells us what you need and what we can deliver. No BDR handoff, no sales engineer theater — the person you talk to is the person who scopes the work.

Talk to an expertBook a demo
Responses in < 1 business day