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?
The only host prerequisite is Docker with Docker Compose:
COMPOSE_PROFILES=vulnerable ALLOW_VULNERABLE_DEMO=true \
docker compose run --rm demoThe 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 --verboseChoose a single scenario interactively:
COMPOSE_PROFILES=vulnerable ALLOW_VULNERABLE_DEMO=true \
docker compose run --rm demo uv run reservation-bola-demo interactiveThe local and GitHub Actions quality boundary is the same Compose command:
docker compose run --rm verifyIt runs the security regression matrix, Ruff, and strict mypy inside the image. No host Python,
uv, pytest, Ruff, mypy, or project dependency is required.
The secure API is the only service started by the default Compose path:
docker compose up --buildOpenAPI 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 vulnerableIts 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.
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.
- 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.
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.
This project is open-source software available under the MIT License.