Insecure Design in Fastapi
How Insecure Design Manifests in Fastapi
Insecure design in Fastapi applications often emerges from architectural decisions that prioritize functionality over security. Fastapi's async-first design and automatic OpenAPI generation can create hidden attack surfaces when developers don't consider security implications during the design phase.
One common manifestation is improper dependency injection. Fastapi's dependency injection system is powerful but can be misused. Consider this vulnerable pattern:
from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_async_db
app = FastAPI()
@app.get('/users/{user_id}')
async def get_user(user_id: int, db: AsyncSession = Depends(get_async_db)):
# No authorization check - any authenticated user can access any user data
user = await db.get(User, user_id)
return user
This design assumes all authenticated users should have access to all user data, violating the principle of least privilege. Fastapi's dependency injection makes it easy to forget authorization checks when focusing on building the data access layer.
Another Fastapi-specific design flaw involves Pydantic model exposure. Fastapi automatically generates OpenAPI schemas from Pydantic models, but developers often expose internal models without considering what data they reveal:
from pydantic import BaseModel
from fastapi import FastAPI
class User(BaseModel):
id: int
email: str
password_hash: str # Exposed in API schema!
is_admin: bool
app = FastAPI()
@app.get('/users/me', response_model=User)
async def get_current_user(current_user: User = Depends(get_current_user)):
return current_user
The password_hash and is_admin fields are exposed in the OpenAPI documentation, revealing internal implementation details. Fastapi's automatic schema generation means sensitive fields can be inadvertently documented.
Fastapi's background tasks feature can also introduce insecure design patterns. Background tasks run outside the request context, making it easy to create race conditions or bypass security checks:
from fastapi import BackgroundTasks
@app.post('/process-data')
async def process_data(data: dict, background_tasks: BackgroundTasks):
# Background task has no context about the original request
background_tasks.add_task(process_in_background, data)
return {'status': 'processing'}
async def process_in_background(data: dict):
# No authentication/authorization context available
await expensive_operation(data)
The background task cannot verify the original request's authentication context, creating a design gap where security boundaries are crossed.
Fastapi-Specific Detection
Detecting insecure design in Fastapi applications requires examining both the code structure and the generated OpenAPI specifications. middleBrick's Fastapi-specific scanning identifies these patterns by analyzing the runtime behavior and comparing it against the OpenAPI schema.
For dependency injection vulnerabilities, middleBrick traces the dependency graph to identify endpoints that lack proper authorization checks. It analyzes the dependency injection patterns to find where database sessions or other sensitive resources are injected without proper access controls:
# middleBrick detection pattern
# Looks for endpoints with database dependencies but no authorization
@app.get('/vulnerable/{id}')
async def vulnerable_endpoint(id: int, db: AsyncSession = Depends(get_db)):
# Missing authorization check - detected as insecure design
return await db.get(SomeModel, id)
The scanner identifies that the endpoint accepts a database dependency but doesn't verify whether the requester has permission to access the specific resource.
For Pydantic model exposure, middleBrick analyzes the OpenAPI schema generation to identify sensitive fields that are unnecessarily exposed. It cross-references the schema with common sensitive field patterns:
# middleBrick analysis
# Detects exposed sensitive fields in response models
class ExposedModel(BaseModel):
id: int
password_hash: str # Detected as sensitive exposure
ssn: str # Detected as sensitive exposure
internal_notes: str # Detected as sensitive exposure
The scanner flags models containing fields like password_hash, ssn, internal_notes, or other PII that shouldn't be exposed through the API.
middleBrick also tests for background task security gaps by analyzing how background tasks are implemented and whether they maintain proper security context. It looks for patterns where background tasks might execute operations without proper authorization:
# middleBrick detection
# Identifies background tasks without security context
@app.post('/process')
async def process(data: dict, background_tasks: BackgroundTasks):
background_tasks.add_task(risky_operation, data)
return {'status': 'queued'}
async def risky_operation(data: dict):
# No auth context - potential security gap
await process_sensitive_data(data)
The scanner flags background tasks that could potentially execute operations without the original request's security context.
Fastapi-Specific Remediation
Fastapi provides several native mechanisms to address insecure design patterns. For dependency injection authorization, Fastapi's dependency system can be enhanced with proper access control:
from fastapi import Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_async_db
from models import User, get_user_by_id
from auth import get_current_active_user
async def get_resource_with_auth(
resource_id: int,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_async_db)
):
"""Dependency that ensures user has access to resource"""
resource = await db.get(Resource, resource_id)
if not resource:
raise HTTPException(status_code=404, detail="Resource not found")
if not await check_user_access(current_user, resource):
raise HTTPException(status_code=403, detail="Access denied")
return resource
@app.get('/resources/{resource_id}')
async def get_resource(resource: Resource = Depends(get_resource_with_auth)):
return resource
This pattern ensures authorization is enforced at the dependency level, making it impossible to access resources without proper permissions.
For Pydantic model exposure, Fastapi's response_model_exclude and response_model_include parameters provide fine-grained control over what data is exposed:
from pydantic import BaseModel
from fastapi import FastAPI
class UserInternal(BaseModel):
id: int
email: str
password_hash: str
is_admin: bool
ssn: str
internal_notes: str
class UserPublic(BaseModel):
id: int
email: str
app = FastAPI()
@app.get('/users/me', response_model=UserPublic)
async def get_current_user(current_user: UserInternal = Depends(get_current_user)):
return current_user
@app.get('/users/{user_id}', response_model=UserInternal,
response_model_exclude={'password_hash', 'ssn', 'internal_notes'})
async def get_user(user_id: int, db: AsyncSession = Depends(get_async_db)):
return await db.get(User, user_id)
This approach separates internal data models from public API contracts, ensuring sensitive fields are never exposed.
For background task security, Fastapi's BackgroundTasks can be used with proper context passing:
from fastapi import BackgroundTasks, Depends
from auth import get_current_user_id
@app.post('/process-data')
async def process_data(
data: dict,
user_id: int = Depends(get_current_user_id),
background_tasks: BackgroundTasks = None
):
"""Pass user context to background task"""
background_tasks.add_task(
process_in_background,
data,
user_id # Pass security context
)
return {'status': 'processing'}
async def process_in_background(data: dict, user_id: int):
"""Background task with security context"""
# Verify user still has permissions
if not await user_has_permission(user_id, data):
return
await process_data_securely(data, user_id)
This pattern ensures background tasks maintain awareness of the original request's security context.