Skip to content

Repository files navigation

Reservation BOLA Demo

Local educational code only. The vulnerable application is intentionally unsafe and must never be deployed. Every user, token, class, reservation, and endpoint in this repository is fictional. Do not adapt this walkthrough to test a real service.

This container-only project shows Broken Object Level Authorization (BOLA)—also commonly called Insecure Direct Object Reference (IDOR)—in a small reservation API. The vulnerable app authenticates the caller but fails to check that the caller owns the target reservation. The secure app performs that ownership check in its database query.

The walkthrough answers one important question: authentication proves who sent a request, but does the application also authorize that identity to act on this specific object?

Run the scripted comparison

The only host prerequisite is Docker with Docker Compose:

COMPOSE_PROFILES=vulnerable ALLOW_VULNERABLE_DEMO=true \
  docker compose run --rm demo

The one-shot container creates temporary databases, starts local APIs inside the container, makes real localhost HTTP requests, prints before/after tables, and removes the temporary state. It shows three independently fresh outcomes:

Scenario Response Expected result
Vulnerable cross-user cancellation 204 Victim is cancelled; queue position 1 is promoted; attacker moves 4 → 3
Secure cross-user attempt generic 404 One generic JSON rejection event; every reservation and position is unchanged
Secure owner cancellation 204 Owner is cancelled; queue position 1 is promoted atomically

Show the sanitized HTTP exchange (identity, method, path, and response—but never bearer tokens):

COMPOSE_PROFILES=vulnerable ALLOW_VULNERABLE_DEMO=true \
  docker compose run --rm demo uv run reservation-bola-demo compare --verbose

Choose a single scenario interactively:

COMPOSE_PROFILES=vulnerable ALLOW_VULNERABLE_DEMO=true \
  docker compose run --rm demo uv run reservation-bola-demo interactive

Verify everything

The local and GitHub Actions quality boundary is the same Compose command:

docker compose run --rm verify

It runs the security regression matrix, Ruff, and strict mypy inside the image. No host Python, uv, pytest, Ruff, mypy, or project dependency is required.

Explore the APIs locally

The secure API is the only service started by the default Compose path:

docker compose up --build

OpenAPI is available at http://127.0.0.1:8000/docs. For example, inspect the fictional attacker's own reservation:

DEMO_TOKEN=demo-attacker-token
curl -sS http://127.0.0.1:8000/me/reservations \
  -H "Authorization: Bearer ${DEMO_TOKEN}"

Starting the vulnerable API requires both the opt-in profile and an exact acknowledgement:

COMPOSE_PROFILES=vulnerable ALLOW_VULNERABLE_DEMO=true \
  docker compose up --build vulnerable

Its OpenAPI page is then at http://127.0.0.1:8001/docs. COMPOSE_PROFILES=vulnerable enables the profile; ALLOW_VULNERABLE_DEMO=true separately acknowledges the unsafe code. Omitting or malforming either control fails closed before the vulnerable app is created. Both published ports bind only to loopback.

The supported fictional bearer tokens are:

Identity Token Initial reservation
Vera Victim demo-victim-token confirmed
Wally One demo-wait-1-token waitlist position 1
Winnie Two demo-wait-2-token waitlist position 2
Will Three demo-wait-3-token waitlist position 3
Avery Attacker demo-attacker-token waitlist position 4

Missing, malformed, and unknown credentials all receive the same 401 Unauthorized response and standard bearer challenge. Tokens and authorization headers are never written to application logs.

What the fix changes

Both apps expose POST /reservations/{id}/cancel. The intentionally vulnerable query selects the reservation by UUID alone. The secure query selects a cancellable target by both UUID and the authenticated user's ID. A foreign UUID and a nonexistent UUID therefore produce the same generic 404 without a revealing existence check.

The secure rejection writes one generic JSON event to standard output with a request ID, actor, action, target UUID, and rejected outcome. It deliberately does not say whether the target exists. Successful cancellation and any queue promotion commit in one database transaction; tests inject a failure before commit to prove complete rollback.

UUID unpredictability is useful defense in depth, but it is not authorization. A server must check the caller's permission for every referenced object even when identifiers are hard to guess.

Safety and scope

  • The secure app is the default; vulnerable code requires two explicit startup actions.
  • State inspection is user-scoped through GET /me/reservations; there is no global inspection or test-reset endpoint.
  • The scenario supplies the victim UUID directly. Discovery, enumeration, real credentials, and real service interaction are outside scope.
  • There is no cloud configuration, production authentication, production database, or runtime switch that weakens the secure application.
  • No application, package, image, or hosted demo endpoint is published by this project. The repository makes no production-readiness, compatibility, support-duration, or service-level promise.

Contributing and security

See CONTRIBUTING.md for the fictional-data, containment, and verification rules. Report unintended vulnerabilities through the private process in SECURITY.md. The documented cross-user behavior in the vulnerable application is intentional and is not itself a security report.

License

This project is open-source software available under the MIT License.

About

Container-only educational demonstration of BOLA in a fictional reservation API.

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages