Insecure Direct Object Reference in Fastapi with Api Keys
Insecure Direct Object Reference in Fastapi with Api Keys — how this specific combination creates or exposes the vulnerability
Insecure Direct Object Reference (BOLA/IDOR) occurs when an API exposes internal object references (e.g., numeric IDs or UUIDs) and relies solely on user-supplied input to locate resources without verifying authorization. In FastAPI, this commonly arises when endpoints use path parameters such as user_id or document_id to fetch records from a database. If authorization checks are missing or incomplete, an authenticated user can manipulate these identifiers to access or modify other users’ data.
When API keys are used for authentication in FastAPI, the risk of BOLA/IDOR persists because API keys typically establish identity (who is making the request) but do not enforce object-level permissions. A key-based authentication flow might validate the key and attach a user context (e.g., current_user) but omit checks to confirm that the requested resource belongs to that user. For example, an endpoint like /users/{user_id}/profile that accepts an API key may authenticate the caller, yet if the route uses user_id from the path directly to query a database without comparing it to the authenticated user’s ID, any caller who knows another user’s ID can access that data.
Consider a FastAPI route that retrieves a user profile by ID:
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
app = FastAPI()
# Simplified dependencies
def get_db():
# returns a session; omitted for brevity
pass
def get_api_key(db: Session = Depends(get_db)):
# validate API key and return associated user_id; simplified
return {"user_id": 100}
@app.get("/users/{user_id}/profile")
def read_profile(user_id: int, api_key_data: dict = Depends(get_api_key), db: Session = Depends(get_db)):
# BOLA risk: user_id from path is used directly without verifying it matches api_key_data["user_id"]
profile = db.query(UserProfile).filter(UserProfile.id == user_id).first()
if not profile:
raise HTTPException(status_code=404, detail="Profile not found")
return profile
In this pattern, api_key_data identifies the caller, but the endpoint trusts user_id from the URL. An attacker with a valid API key can change {user_id} to access any profile, provided they know or guess the ID. This is a classic BOLA/IDOR: authentication is present (API key), but authorization on the object is missing.
BOLA/IDOR can also manifest with UUIDs if the mapping between UUIDs and user ownership is not validated. For example, an endpoint fetching documents by UUID should verify that the authenticated API key’s user owns the document before returning it. Without such checks, the API exposes references that should be opaque to clients.
Because middleBrick tests unauthenticated attack surfaces and includes Authentication and BOLA/IDOR checks among its 12 parallel security checks, it can detect such authorization gaps even when API keys are present. The scanner does not fix the logic but highlights the exposure and provides remediation guidance to help developers enforce proper ownership verification.
Api Keys-Specific Remediation in Fastapi — concrete code fixes
To prevent BOLA/IDOR when using API keys in FastAPI, ensure that every data access operation validates that the requested resource belongs to the authenticated principal derived from the API key. Below are concrete, safe patterns.
1. Compare path identifiers with authenticated identity
Before querying the database, compare the path parameter with the user identity associated with the API key. This ensures users can only access their own resources.
from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session
from typing import Dict
def get_api_key_user_id(db: Session) -> int:
# Validate API key and return the associated user_id; implementation omitted
# Returns the user_id extracted from a valid key, or raises an exception if invalid
return 100
@app.get("/users/{user_id}/profile")
def read_profile(
user_id: int,
api_key_data: Dict[str, int] = Depends(get_api_key_user_id),
db: Session = Depends(get_db)
):
authenticated_user_id = api_key_data["user_id"]
if authenticated_user_id != user_id:
raise HTTPException(status_code=403, detail="Access denied: cannot access other users’ resources")
profile = db.query(UserProfile).filter(UserProfile.id == user_id).first()
if not profile:
raise HTTPException(status_code=404, detail="Profile not found")
return profile
This pattern explicitly rejects requests where the path ID does not match the authenticated user’s ID, closing the BOLA vector.
2. Use ownership checks with UUIDs
When identifiers are opaque (e.g., UUIDs), fetch the resource and verify ownership against the API key’s user.
from uuid import UUID
from sqlalchemy.orm import Session
from sqlalchemy.exc import NoResultFound
@app.get("/documents/{document_id}")
def get_document(
document_id: UUID,
api_key_data: Dict[str, int] = Depends(get_api_key_user_id),
db: Session = Depends(get_db)
):
authenticated_user_id = api_key_data["user_id"]
# Assume Document has fields: id (UUID), owner_user_id (int)
document = db.query(Document).filter(
Document.id == document_id,
Document.owner_user_id == authenticated_user_id
).first()
if not document:
raise HTTPException(status_code=404, detail="Document not found or access denied")
return document
By including Document.owner_user_id == authenticated_user_id in the filter, the database returns None if the document does not belong to the caller, effectively preventing IDOR.
3. Centralize authorization logic
For larger applications, encapsulate ownership checks in reusable functions or dependency overrides to avoid repetition and mistakes.
def user_resource_owner(resource_type: str):
def dependency(user_id: int, db: Session = Depends(get_db)):
# Example: verify that a resource exists and belongs to user_id
# Implement generic checks based on resource_type
pass
return Depends(dependency)
@app.get("/records/{record_id}")
def fetch_record(
record_id: int,
owner_id: int = user_resource_owner("record"),
db: Session = Depends(get_db)
):
record = db.query(Record).filter(Record.id == record_id, Record.owner_id == owner_id).first()
if not record:
raise HTTPException(status_code=404, detail="Record not found or unauthorized")
return record
These remediation examples demonstrate how to combine API key authentication with explicit object-level authorization. middleBrick’s BOLA/IDOR checks will flag missing ownership verifications; applying these patterns will resolve the findings and reduce the risk of unauthorized data access.
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |