flawopen.com/bola-idor/Python
Learn how to eliminate BOLA (CWE-639) / IDOR vulnerabilities in Python and FastAPI by scoping database queries directly to authenticated tenant identities rather than trusting client-supplied IDs.
Imagine checking into Room 204 at a hotel and receiving a valid keycard. But you discover that if you scratch out '204' on the door handle and write '205', the lock clicks open and allows you to rummage through another guest's luggage. The hotel confirmed you were a registered guest (authentication), but never checked whether your key matched Room 205 (object authorization). In BOLA/IDOR, an attacker changes an ID in an API request and accesses another user's private records.
Authentication vs. AuthorizationBOLA (Broken Object Level Authorization)IDOR (Insecure Direct Object Reference)CWE-639) where internal implementation objects are exposed directly to users without access control checks.Tenancy Scope InjectionNon-Enumerable Identifiers (UUIDv4)An attacker creates a legitimate account on the platform and receives a valid JWT authentication bearer token.
The attacker accesses their own billing invoice via GET /api/invoices/1042 and observes sequential database identifiers in use.
The attacker modifies the URL to request GET /api/invoices/1041 using their own valid authentication token.
The FastAPI backend confirms the token is valid, but queries the database solely by Invoice.id == 1041 without verifying tenant ownership.
The server returns confidential invoice records, billing details, and personal data belonging to another organization.
The attacker scripts a loop iterating over IDs 1 through 100,000, draining the entire multi-tenant database.
# VULNERABLE: Verifies user login, but queries object solely by client ID
from fastapi import FastAPI, Depends, HTTPException, status
from sqlalchemy.orm import Session
@app.get("/api/invoices/{invoice_id}")
def get_invoice(invoice_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
# Flaw: Attacker passes invoice_id belonging to another tenant
invoice = db.query(Invoice).filter(Invoice.id == invoice_id).first()
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
# Leaks confidential billing records of competitor organizations!
return invoice
# HARDENED: Query is strictly bound to the authenticated tenant's organization ID
from fastapi import FastAPI, Depends, HTTPException, status
from sqlalchemy.orm import Session
@app.get("/api/invoices/{invoice_id}")
def get_invoice(invoice_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
# Defense-in-depth: query filters by BOTH invoice_id AND authenticated organization_id
invoice = db.query(Invoice).filter(
Invoice.id == invoice_id,
Invoice.organization_id == current_user.organization_id
).first()
if not invoice:
# Return 404 rather than 403 to prevent object existence enumeration
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invoice not found")
return invoice