Broken Object Level Authorization

How to Fix Broken Object Level Authorization in FastAPI March 2026 [CVE-2026-2975]

[Fixed Jun 2026] Updated CVE-2026-2975

Overview

Broken Object Level Authorization (BOLA) occurs when an application fails to enforce access controls on a per-object basis, allowing an attacker to access or manipulate resources they should not. In FastAPI-based admin tooling like FastApiAdmin, insufficient object-level checks can lead to information disclosure, unauthorized file access, or unrestricted uploads. The CVEs below demonstrate how FastApiAdmin up to 2.2.0 allowed such behavior: CVE-2026-2975 (Custom Documentation Endpoint) could disclose internal API docs; CVE-2026-2976 (Download Endpoint) enabled reading arbitrary files via manipulated file_path; and CVE-2026-2977 (Scheduled Task API Upload) allowed unbounded uploads. These issues align with CWE-200 (Information Disclosure), CWE-284 (Improper Access Control), and CWE-434 (Unrestricted Upload of Files). In practice, attackers may exploit these remotely, especially when auth checks are missing or input is unsafely used to derive file paths or destinations. To mitigate, implement per-object authorization checks, validate and normalize all inputs, restrict file system operations to a known base directory, and protect sensitive endpoints with strong authentication and role-based access control. The fixes shown are concrete FastAPI patterns you can adopt in real code to close these gaps.

Affected Versions

FastApiAdmin <= 2.2.0

Code Fix Example

FastAPI API Security Remediation
VULNERABLE PATTERN (illustrative, insecure):
from fastapi import FastAPI, Depends, HTTPException
from fastapi.responses import FileResponse
from pathlib import Path
from fastapi import UploadFile

app = FastAPI()
BASE_DIR = Path("/app/files")

class User:
    def __init__(self, username: str, is_admin: bool = False):
        self.username = username
        self.is_admin = is_admin

def get_current_user():
    # Placeholder for real authentication
    return User("alice", is_admin=False)

# Vulnerable: reset_api_docs without proper auth and unsafe docs handling
@app.post("/admin/reset_api_docs")
def reset_api_docs():
    # resets internal docs without verifying privileges
    return {"status": "docs reset"}

# Vulnerable: direct file download using unvalidated path
@app.get("/download/{file_path}")
def download_controller(file_path: str, user: User = Depends(get_current_user)):
    target = BASE_DIR / file_path  # no path traversal protection
    if not target.exists():
        raise HTTPException(status_code=404)
    return FileResponse(target)

# Vulnerable: file upload without validation
@app.post("/upload")
async def upload_controller(file: UploadFile, dest: str = ""):
    location = BASE_DIR / dest
    with open(location / file.filename, "wb") as f:
        content = await file.read()
        f.write(content)
    return {"status": "uploaded"}

# FIXED PATTERN: with per-object authorization, path validation, and restricted uploads
from fastapi import Depends, HTTPException
import uuid
import os

# Reusable admin check
def get_current_admin():
    user = get_current_user()
    if not user.is_admin:
        raise HTTPException(status_code=403)
    return user

def is_safe_path(base: Path, target: Path) -> bool:
    try:
        return str(target.resolve()).startswith(str(base.resolve()))
    except Exception:
        return False

OWNER_MAP = {
    "secret/secret.txt": "admin",
    "public/readme.txt": "alice",
}

@app.post("/admin/reset_api_docs_secure")
def reset_api_docs_secure(user: User = Depends(get_current_admin)):
    return {"status": "docs reset"}

@app.get("/download_secure/{file_path}")
def download_controller_secure(file_path: str, user: User = Depends(get_current_user)):
    target = (BASE_DIR / file_path).resolve()
    if not is_safe_path(BASE_DIR, target):
        raise HTTPException(status_code=403)
    rel = target.relative_to(BASE_DIR).as_posix()
    owner = OWNER_MAP.get(rel)
    if owner and owner != user.username and not user.is_admin:
        raise HTTPException(status_code=403)
    if not target.exists():
        raise HTTPException(status_code=404)
    return FileResponse(target)

ALLOWED_EXT = {".txt", ".pdf", ".png"}

@app.post("/upload_secure")
async def upload_controller_secure(file: UploadFile, dest_dir: str = "", user: User = Depends(get_current_user)):
    if file.filename == "":
        raise HTTPException(status_code=400)
    if Path(file.filename).suffix not in ALLOWED_EXT:
        raise HTTPException(status_code=400)
    target_dir = (BASE_DIR / dest_dir).resolve()
    if not is_safe_path(BASE_DIR, target_dir):
        raise HTTPException(status_code=400)
    filename = uuid.uuid4().hex + Path(file.filename).suffix
    target = target_dir / filename
    with open(target, "wb") as f:
        content = await file.read()
        f.write(content)
    return {"saved_to": str(target)}

CVE References

Choose which optional cookies to allow. You can change this any time.