Overview
CVE-2024-40627 describes a vulnerability in the Open Policy Agent (OPA) middleware used with FastAPI (often via the fastapi-opa integration) where HTTP OPTIONS requests bypassed authentication and policy evaluation. This allowed unauthenticated clients to probe endpoints and infer information about resource existence, potentially leading to targeted information disclosure. The vulnerability can enable an attacker to enumerate entities that exist in an application by observing how OPTIONS requests are handled, which is a form of information exposure (CWE-204). The issue has been addressed in release 2.0.1 of the OPA middleware, and users are advised to upgrade. There are no well-known workarounds for this specific bypass during OPTIONS, so upgrading is the recommended remediation. In the context of FastAPI, this vulnerability demonstrates how misconfigurations or outdated policy middleware can create an attack surface where sensitive existence information about resources is leaked via unauthenticated OPTIONS traffic.
Affected Versions
fastapi-opa middleware <= 2.0.0 (pre-2.0.1); fixed in 2.0.1
Code Fix Example
FastAPI API Security Remediation
import threading
from fastapi import FastAPI
import uvicorn
# Demonstration middlewares: Vulnerable (OPTIONS bypass) vs Fixed (OPTIONS also requires auth)
class VulnerableOPAMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope.get('type') != 'http':
await self.app(scope, receive, send)
return
if scope.get('method') == 'OPTIONS':
# Vulnerable: OPTIONS bypasses authentication/policy evaluation
await self.app(scope, receive, send)
return
headers = {k.decode().lower(): v for k, v in scope.get('headers', [])}
if 'authorization' not in headers:
await send({"type": "http.response.start", "status": 401, "headers": [(b'content-type', b'application/json')]})
await send({"type": "http.response.body", "body": b'{"detail":"Unauthorized"}', "more_body": False})
return
await self.app(scope, receive, send)
class FixedOPAMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope.get('type') != 'http':
await self.app(scope, receive, send)
return
headers = {k.decode().lower(): v for k, v in scope.get('headers', [])}
if 'authorization' not in headers:
await send({"type": "http.response.start", "status": 401, "headers": [(b'content-type', b'application/json')]})
await send({"type": "http.response.body", "body": b'{"detail":"Unauthorized"}', "more_body": False})
return
await self.app(scope, receive, send)
from fastapi import FastAPI
# Shared resource endpoint for demonstration
app_vuln = FastAPI()
app_vuln.add_middleware(VulnerableOPAMiddleware)
@app_vuln.get('/entities/{entity_id}')
def read_entity(entity_id: int):
return {'entity_id': entity_id, 'exists': True}
app_fix = FastAPI()
app_fix.add_middleware(FixedOPAMiddleware)
@app_fix.get('/entities/{entity_id}')
def read_entity_fix(entity_id: int):
return {'entity_id': entity_id, 'exists': True}
if __name__ == '__main__':
# Run both apps on different ports to compare behavior
def run_vuln():
uvicorn.run(app_vuln, host='0.0.0.0', port=8000)
threading.Thread(target=run_vuln, daemon=True).start()
uvicorn.run(app_fix, host='0.0.0.0', port=8001)