flawopen.com/bola-idor/Python
FastAPI 및 Python 환경에서 BOLA (CWE-639) / IDOR 취약점을 근절하는 방법: 클라이언트가 전달한 객체 ID를 맹신하지 않고 인증된 조직 테넌트 ID를 쿼리에 강제 결합하는 실무 가이드.
호텔 204호에 투숙했는데 문 손잡이에 205를 적기만 해도 옆 방 문이 열리는 상황과 같습니다. 호텔은 투숙객 신원(인증)은 확인했지만, 해당 방의 소유자(객체 권한 부여)인지는 확인하지 않았습니다.
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