HIGH insecure direct object referencefastapiapi keys

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 IDNameSeverity
CWE-250Execution with Unnecessary Privileges HIGH
CWE-639Insecure Direct Object Reference CRITICAL
CWE-732Incorrect Permission Assignment HIGH

Frequently Asked Questions

Can API keys alone prevent BOLA/IDOR vulnerabilities in FastAPI?
No. API keys can authenticate a caller but do not enforce object-level permissions. Developers must explicitly verify that the authenticated identity matches the requested resource owner; otherwise, BOLA/IDOR remains possible.
Will enabling API key authentication remove BOLA/IDOR findings in middleBrick scans?
Not automatically. middleBrick’s BOLA/IDOR checks look for missing ownership validation. Adding API key authentication is helpful, but you must also ensure endpoints compare path identifiers with the authenticated principal and deny access on mismatch.