GuardAPI

First-party note · 10 September 2026

How to fail a PR when tenant B can read tenant A

Broken Object Level Authorization (BOLA), also called IDOR, is still API1 in the OWASP API Security Top 10:2023. The request looks legitimate. The bug is that the server loads the object you named without checking that your tenant owns it. WAFs and single-user scanners cannot see it. Two identities can.

The check, in one sequence

  1. Parse the OpenAPI spec in the pull request.
  2. Find pairs GET /collection + GET /collection/{id}. Skip nested paths that still have a parent parameter. Stop at 40 pairs.
  3. As tenant A, list. Harvest a real object id and unique string markers (email, memo, name).
  4. As tenant B, GET that object.
  5. Fail the job only if B’s HTTP 2xx body contains A’s id or those markers. Also fail if a 401/403/404 body still leaks them.
  6. 401/403/404 without those fields: pass. 200 without owner evidence: inconclusive, do not block merge. Bad tokens: error, never a green check.

That sequence is what GuardAPI v6 runs in GitHub Actions. You can implement it yourself with two curls. The product is the pairing, the conservative verdict, SARIF, and a dashboard row — not a model opinion.

What a fail looks like

Example finding Illustrative fixture. Not a customer scan.

bola / GuardAPI BOLA Gate

failed in 12s

GET /invoices/inv_a8f2 as TOKEN_B
HTTP 200
body contains id "inv_a8f2" owned by tenant A

verdict: fail
reason: proven cross-tenant leak
{
  "id": "inv_a8f2",
  "tenant_id": "org_acme",
  "amount_cents": 129900,
  "memo": "Q3 retainer — Acme"
}

Fail only if B’s 2xx body contains A’s id or unique markers. 401 / 403 / 404 without those fields pass. 200 without owner evidence is inconclusive and does not block merge.

What does not belong in this gate

  • LLM grades of the OpenAPI file. Hallucinated “medium BOLA risk” is not evidence.
  • HSTS, CORS, SSL, or cookie flags. Those are a different job.
  • Mutating methods. GET-only keeps the Action from deleting a staging invoice because a path parameter lined up.
  • Failing on 200 with an empty body. That is how teams disable the check after one noisy week.

Wire it on GitHub

- uses: GuardAPI/ghost-api@v6
  with:
    api-key: ${{ secrets.GUARD_API_KEY }}
    openapi-path: ./openapi.json
    base-url: ${{ secrets.STAGING_API_URL }}
    token-a: ${{ secrets.TOKEN_USER_A }}
    token-b: ${{ secrets.TOKEN_USER_B }}

Full install, OpenAPI constraints, and troubleshooting: docs. Trial is 14 days on one repo. Sign in.

If you would rather not buy this

Write the same probe. Or use an OSS matrix tester — we compared them fairly. The reason this page exists is so “BOLA in CI” has a concrete fail rule, not another platform homepage.

FAQ

Is BOLA the same as IDOR?

In practice, yes. OWASP API1:2023 uses BOLA. Older web-app language used IDOR. Cross-tenant read of /invoices/{id} is the same bug.

Why not fail every HTTP 200 from tenant B?

A 200 can be an empty envelope, a generic message, or an object B actually owns. GuardAPI fails only when B’s body contains A’s id or unique markers harvested from A’s object.

Does Postgres RLS make this unnecessary?

RLS is good. Handlers still do findById(req.params.id) and skip the tenant predicate. The merge gate observes HTTP, not SQL.