flawopen.com/bola-idor/Python
彻底解决 Python 与 FastAPI 中的 BOLA (CWE-639) / IDOR 越权漏洞:在 ORM 查询层强制绑定当前认证租户组织 ID,拒绝盲目信任前端传递的对象主键。
就像住酒店拿到 204 房卡,发现把房门号改成 205 就能开门偷看别人的行李。酒店验证了你是注册住客(身份认证),却没有验证你是否有权进入 205 号房间(对象授权)。BOLA 就是攻击者修改 API 中的 ID 查看他人隐私。
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