Skip to main content

Authorization contract testing for IDOR/BOLA — prove user A can't touch user B's objects, in CI.

Project description

AuthzTrace

Authorization tests for IDOR/BOLA bugs.

AuthzTrace proves that one user cannot access another user's objects by replaying an authorization contract against your real API.

status license OWASP API Python

Most scanners cannot reliably find IDOR/BOLA because they do not know ownership. They can see /api/invoices/inv_123, but they do not know that inv_123 belongs to Alice and must not be readable by Bob.

AuthzTrace makes that missing context explicit:

Alice owns inv_alice_001
Bob owns inv_bob_002

AuthzTrace checks:
- Alice can read Alice's invoice
- Bob cannot read Alice's invoice
- Anonymous users cannot read either invoice
- The same policy holds for path IDs, query IDs, JSON body IDs, and more
flowchart LR
    A[Ownership contract] --> B[Generated actor x object matrix]
    B --> C[Preflight setup checks]
    C --> D[Authorization attack rows]
    D --> E[SARIF, JSON, JUnit, terminal]
    E --> F[Fail the PR on real BOLA]

Why It Exists

Broken Object Level Authorization is the classic IDOR problem: changing an object ID lets one user reach another user's data. It is still hard to catch automatically because authorization is business logic.

AuthzTrace is not a crawler. It is closer to a contract test:

You declare AuthzTrace proves
Actors and auth tokens Each identity can authenticate
Object IDs and owners Owners can access their own objects
API endpoints Other users are denied
Response markers Denied responses do not leak protected data

If credentials or test fixtures are broken, AuthzTrace exits with a setup error instead of giving a false green result.

Quick Demo

Install AuthzTrace:

pip install authztrace

For local development from this repo:

git clone https://github.com/Asttr0/AuthzTrace.git
cd AuthzTrace
pip install -e .
pip install -r examples/vulnerable-api/requirements.txt

Start the deliberately vulnerable demo API:

python examples/vulnerable-api/app.py

In another terminal, run AuthzTrace:

export ALICE_TOKEN=alice-token
export BOB_TOKEN=bob-token
authztrace run -c examples/authztrace.yaml

You get a real authorization finding:

RESULT ACTOR      TARGET     EXPECT  STATUS  METHOD  PATH
------------------------------------------------------------------------------------------------
PASS   alice      alice      allow   200     GET     /api/invoices/inv_alice_001
FAIL   bob        alice      deny    200     GET     /api/invoices/inv_alice_001
         -> BOLA: 'bob' accessed alice's invoice (inv_alice_001) - HTTP 200
PASS   anon       alice      deny    401     GET     /api/invoices/inv_alice_001
...
SKIP   alice      alice      allow   -       DELETE  /api/invoices/inv_alice_001
         -> unsafe DELETE skipped in read-only mode
...
12 passed, 6 failed, 0 warnings, 0 errors, 6 skipped, 24 checks
categories: bola=6, unsafe_skipped=6

Stop the first server with Ctrl+C, then run the fixed API:

SECURE=1 python examples/vulnerable-api/app.py
authztrace run -c examples/authztrace.yaml

The same contract becomes green:

18 passed, 0 failed, 0 warnings, 0 errors, 6 skipped, 24 checks
categories: unsafe_skipped=6

Minimal Contract

base_url: http://localhost:3000

actors:
  alice: { auth: { type: bearer, token: "${ALICE_TOKEN}" } }
  bob:   { auth: { type: bearer, token: "${BOB_TOKEN}" } }
  anon:  { auth: { type: none } }

resources:
  invoice:
    ids:
      alice: inv_alice_001
      bob: inv_bob_002
    markers:
      alice: "Alice private invoice"
      bob: "Bob private invoice"
    endpoints:
      - name: read invoice
        request: GET /api/invoices/{id}
        assertions:
          allow_contains: ["{marker}"]
          deny_not_contains: ["{marker}"]

policy:
  default: owner-only
  deny_status: [401, 403, 404]

From this, AuthzTrace generates the full matrix:

alice -> alice invoice  should allow
bob   -> alice invoice  should deny
anon  -> alice invoice  should deny
alice -> bob invoice    should deny
bob   -> bob invoice    should allow
anon  -> bob invoice    should deny

Endpoint Shapes

Object IDs can live almost anywhere:

endpoints:
  - request: GET /api/invoices/{id}

  - method: GET
    path: /api/invoices
    query:
      id: "{id}"

  - method: POST
    path: /api/invoices/lookup
    safe: true
    json:
      invoice_id: "{id}"

Exact placeholders preserve their type. If an ID is numeric, invoice_id: "{id}" sends a number, not a string.

For endpoints that are intentionally shared, override the default owner-only rule:

- request: GET /api/admin/invoices/{id}
  allow: [owner, admin]

- request: GET /api/team/invoices/{id}
  allow: [authenticated]

Safe By Default

AuthzTrace is designed for CI, so it does not execute mutating endpoints by default.

Method Default
GET, HEAD, OPTIONS executed
POST, PUT, PATCH, DELETE skipped

Mark read-like POST endpoints as safe:

- method: POST
  path: /api/search
  safe: true

Run unsafe endpoints only when you have disposable test data:

authztrace run -c authztrace.yaml --include-unsafe

Skipped endpoints are reported as SKIP, not counted as passes.

CI Usage

AuthzTrace returns clear exit codes:

Code Meaning
0 clean
1 real finding, or warning in --strict mode
2 setup or operational failure

Generate SARIF for GitHub code scanning:

authztrace run -c authztrace.yaml --sarif authztrace.sarif

Or use the composite GitHub Action:

- uses: Asttr0/AuthzTrace@v0.3.1
  with:
    config: authztrace.yaml
    sarif: authztrace.sarif
    strict: "true"
    include-unsafe: "false"

JSON and JUnit are also available:

authztrace run -c authztrace.yaml \
  --json authztrace.json \
  --junit authztrace.xml

Generate A Starter Contract

You can scaffold a contract from OpenAPI:

authztrace init --from openapi.yaml --output authztrace.yaml

The generator is conservative. It detects simple single-object endpoints such as:

GET /api/invoices/{invoice_id}
GET /api/invoices?id=...

You still review the generated IDs, actors, and ownership before using it in CI.

What It Catches

AuthzTrace currently covers:

  • horizontal IDOR/BOLA
  • anonymous object access
  • object IDs in path, query, headers, JSON body, and form body
  • denied responses that leak protected markers
  • allowed responses that return the wrong body
  • privileged actors through endpoint allow rules
  • unsafe endpoint skipping for CI safety
  • setup failures from broken credentials or invalid fixtures

The full pattern roadmap lives in docs/CORPUS.md.

Why Not A Normal Scanner?

Capability Generic scanner AuthzTrace
Knows object ownership no yes
Tests cross-user access weak yes
Runs in CI sometimes yes
Produces SARIF sometimes yes
Keeps the security rule next to code no yes

The point is simple: if your API says Bob can read Alice's invoice, AuthzTrace should make the pull request fail.

Status

Alpha. The core engine, contract format, OpenAPI starter generator, terminal/SARIF/JSON/JUnit output, GitHub Action, read-only safety model, and demo API are working end to end.

Next priorities:

  • login-flow auth
  • nested parent-child ownership
  • GraphQL BOLA checks
  • baselines for accepted deviations

License

MIT (c) 2026 Mohamed Taha Slimani (@Asttr0)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

authztrace-0.3.1.tar.gz (23.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

authztrace-0.3.1-py3-none-any.whl (19.8 kB view details)

Uploaded Python 3

File details

Details for the file authztrace-0.3.1.tar.gz.

File metadata

  • Download URL: authztrace-0.3.1.tar.gz
  • Upload date:
  • Size: 23.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for authztrace-0.3.1.tar.gz
Algorithm Hash digest
SHA256 e814ba9ba6936e572946f98962f0174a1faa53a1c185bb3668c73fd716e30ff4
MD5 dba9afdefd86a900f53130be7990fee2
BLAKE2b-256 3a5e293492b43e6fe19fdee2a97918c6e41845cdda8a4e97d42802122f7e2b38

See more details on using hashes here.

Provenance

The following attestation bundles were made for authztrace-0.3.1.tar.gz:

Publisher: release.yml on Asttr0/AuthzTrace

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file authztrace-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: authztrace-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 19.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for authztrace-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 0688e298674236712054c083334830bf476de1aaed5880daf09d83d1aec47588
MD5 32558e03cd5aa0c0a13e673bc50a7175
BLAKE2b-256 2867024ad1c4bf2be6cb663e38ef447b3612a9ecce736b0f99eae7f069b1431c

See more details on using hashes here.

Provenance

The following attestation bundles were made for authztrace-0.3.1-py3-none-any.whl:

Publisher: release.yml on Asttr0/AuthzTrace

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page