---
title: "OpenAPI IDOR & BOLA Scanner"
canonical: https://trickest.com/library/api-idor-and-bola-scanner-openapi
description: "Read a target's OpenAPI spec, enumerate every GET that takes an object id, and probe each with altered ids and stripped auth."
---

# OpenAPI IDOR & BOLA Scanner

## Overview

Broken object level authorization sits at the top of the OWASP API Security Top
10, and it is the class a scanner is worst at, because every request is
individually valid. The endpoint exists, the id is well-formed, the response is a
200. There is no signature to match, only an object that belonged to somebody
else.

So the useful question is not what the bug is, it is which operations you managed
to test for it. This drives the test from the API's own specification. You get
every GET operation that takes an object reference, probed with varied ids and
with authorization removed, and a list of the objects that came back when they
should not have. Coverage follows the API as it grows rather than trailing it,
which makes it worth running on every release as
[continuous security testing](/use-cases/continuous-security-testing).

That enumeration step is the whole difference. A crawl or a proxy recording only
reaches the routes something already called, so the endpoints added last sprint,
the admin ones nobody links to, and the ones only a mobile client touches never
get tested. The spec lists them the day they ship, names the parameter that
carries the object reference, and supplies the example values needed to build a
request the server will accept. Testing what the API says it offers beats testing
what you happened to observe.

It is GET-only by design. Nothing here writes, so a run cannot create, modify or
delete an object in the API under test.

## Pipeline

1. Read the OpenAPI or Swagger specification URL.
2. The spec is fetched and parsed.
3. Every GET operation taking an object reference is enumerated, with its
   parameters and expected responses.
4. Each operation is requested with adjacent and canary ids and with
   authorization stripped, distributed across the fleet.
5. Objects that returned without authorization are written to a PDF report.

## Inputs

- **OpenAPI spec URL.** The Swagger 2.0 or OpenAPI 3.x document describing the
  API under test. The run drives entirely off it, so there is no endpoint list to
  maintain by hand. The default is the public Swagger Petstore, an API published
  for testing, so a first run has a target you are allowed to hit.
- **Bearer token.** Optional, and empty by default. Left empty, every probe is
  unauthenticated. Supply one and the same operations are also requested as a
  logged-in caller, which is how cross-object access gets tested.
- **Object id variations.** The altered ids each operation is probed with,
  adjacent and canary values by default.

## Outputs

- **Operation inventory.** Every GET operation in the spec that takes an object
  reference, with the parameter carrying it, so you can see the tested surface
  before you read a single finding.
- **Probe results.** Each request as sent, paired with the object that came
  back. An unauthenticated 2xx carrying object data is the finding, and the
  recorded request replays by hand.
- **PDF report.** Spec coverage, the object-id endpoints tested, and the IDOR
  and BOLA findings ranked by severity.

## Sample output

From a completed run against `petstore.swagger.io`, the API published for testing.

Three of the eight GET operations take an object reference, and each is
enumerated with the parameter that carries it:

```jsonl
{"op_id":"getPetById","method":"GET","path":"/pet/{petId}","param":"petId","param_in":"path","param_type":"integer","filled_path":"/pet/1"}
{"op_id":"getOrderById","method":"GET","path":"/store/order/{orderId}","param":"orderId","param_in":"path","param_type":"integer","filled_path":"/store/order/1"}
{"op_id":"getUserByName","method":"GET","path":"/user/{username}","param":"username","param_in":"path","param_type":"string","filled_path":"/user/user1"}
{"op_id":"getUserByName","method":"GET","path":"/user/{username}","param":"username","param_in":"path","param_type":"string","filled_path":"/user/user2"}
```

Parsing the spec produces the coverage figure before a single request is sent:

```json
{"total_paths": 14, "get_operations": 8, "objid_targets": 3, "title": "Swagger Petstore", "version": "1.0.7"}
```

The probe records each request as sent and what came back to a caller carrying no
token at all:

```json
{"op_id": "getPetById", "param": "petId", "had_token": false, "probes": [
  {"val": 1,  "url": "https://petstore.swagger.io/v2/pet/1",  "is_baseline": true,  "unauth": {"status": 200, "len": 138, "hash": "38eaad3f38a5", "ctype": "application/json"}},
  {"val": 2,  "url": "https://petstore.swagger.io/v2/pet/2",  "is_baseline": false, "unauth": {"status": 404, "len": 51,  "hash": "0320cbce8433", "ctype": ""}},
  {"val": 5,  "url": "https://petstore.swagger.io/v2/pet/5",  "is_baseline": false, "unauth": {"status": 200, "len": 135, "hash": "a0a1460a9b8a", "ctype": "application/json"}},
  {"val": 9,  "url": "https://petstore.swagger.io/v2/pet/9",  "is_baseline": false, "unauth": {"status": 200, "len": 134, "hash": "f344c7420ef0", "ctype": "application/json"}},
  {"val": 10, "url": "https://petstore.swagger.io/v2/pet/10", "is_baseline": false, "unauth": {"status": 200, "len": 138, "hash": "abd0908f6ed9", "ctype": "application/json"}}]}
```

Four ids returned four objects to an unauthenticated caller, and the differing
body hashes are what separate that from one generic response repeated back. The
other two operations answered 404 at every id they were probed with, so the run
records them as tested rather than as findings. That pairing of request and
response is what makes the report actionable, because the argument is never about
whether the endpoint exists, only about who should be able to read it.

## FAQ

### Why drive the test from a spec instead of crawling the API?

A crawl reaches only the routes something already called. The spec lists every
operation on the day it ships, names the parameter that carries the object
reference, and carries example values for the other required parameters, so the
baseline request is one the server accepts rather than one you guessed at.

### How is this different from pointing a vulnerability scanner at the same API?

A scanner matches signatures, and authorization bugs have none. This does not
look for a pattern in a response, it compares who asked with what came back
across the operations the spec says take an object id.

### Does the scan write to the API?

No. Only GET operations are enumerated and probed, so a run cannot create,
modify or delete an object in the API under test.

### Can it test as a logged-in user, not just anonymously?

Yes, by supplying a Bearer token. Note what that gives you: the comparison is
between one authenticated caller and no caller at all, plus ids that are not that
caller's. It is not a two-account diff, so there is no second set of credentials
to configure.

### What does a finding look like?

The request as sent, paired with the object that came back. Either an
unauthenticated 2xx returning object data, or an id belonging to somebody else
returning data. Because the full request is recorded, whoever fixes it can replay
it rather than argue about reproduction steps.

## Related workflows

- [Find SSRF in HTTP Parameters](/library/ssrf-scanner). Reach for this instead when the
  parameter you distrust takes a URL rather than an object id, and the bug is
  where the server fetches rather than who owns the record.
- [Directory & Content Discovery](/library/directory-and-content-discovery).
  Reach for this instead when there is no spec to read and you need to find the
  endpoints before you can test authorization on them.
- [Find Reflected XSS on a Domain](/library/xss-scanner). Reach for this instead when the API
  renders into a browser and the question is what the response does client-side,
  not who it belongs to.

---
_Markdown source of https://trickest.com/library/api-idor-and-bola-scanner-openapi._
