Cors Wildcard in Fastapi
How Cors Wildcard Manifests in Fastapi
CORS wildcard configuration in FastAPI can create serious security vulnerabilities when developers use overly permissive settings. The most common manifestation occurs when developers set origins="*" in their CORS middleware, allowing any domain to make cross-origin requests to their API.
Consider this vulnerable FastAPI code:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
This configuration allows any website to make authenticated requests to your FastAPI backend. The critical issue is the combination of allow_origins="*" with allow_credentials=True. According to the CORS specification, browsers will reject this combination, but some FastAPI versions and browsers may handle it inconsistently.
Attackers can exploit this by hosting malicious websites that make authenticated requests to your FastAPI endpoints. If users visit the malicious site while logged into your FastAPI application, the attacker can:
- Steal session cookies or JWT tokens
- Make API calls on behalf of authenticated users
- Extract sensitive data through timing attacks
- Trigger actions like money transfers or data deletion
Another FastAPI-specific manifestation occurs with improper dependency injection. Consider:
@app.post("/sensitive-data")
async def get_data(
db: Database = Depends(get_db), # Database injected from any origin
user: User = Depends(get_current_user)
):
return db.query_user_data(user.id)
When CORS is misconfigured, the Database dependency can be accessed from any origin, exposing your data layer to cross-origin attacks.
Fastapi-Specific Detection
Detecting CORS wildcard vulnerabilities in FastAPI requires both static analysis and runtime scanning. Here's how to identify these issues:
Static Code Analysis:
import ast
def find_cors_wildcard_issues(file_path):
with open(file_path, 'r') as f:
tree = ast.parse(f.read())
issues = []
for node in ast.walk(tree):
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Attribute):
if node.func.attr == 'add_middleware':
for keyword in node.keywords:
if keyword.arg == 'allow_origins':
if isinstance(keyword.value, ast.List):
for element in keyword.value.elts:
if isinstance(element, ast.Constant) and element.value == '*':
issues.append({
'line': node.lineno,
'message': 'CORS wildcard detected'
})
return issues
Runtime Testing with middleBrick: middleBrick's black-box scanning can detect CORS misconfigurations without requiring source code access. The scanner tests:
- Whether credentials are accepted from arbitrary origins
- HTTP method restrictions
- Header filtering effectiveness
- Preflight request handling
Manual Testing:
# Test with curl
curl -H "Origin: http://evil.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: X-Requested-With" \
--verbose \
https://your-fastapi-app.com
# Check response headers for:
# - Access-Control-Allow-Origin: *
# - Access-Control-Allow-Credentials: true
middleBrick CLI Integration:
# Scan your FastAPI endpoint
middlebrick scan https://api.yourdomain.com --output json
# Check for CORS-related findings in the report
# middleBrick will flag wildcard origins, missing method restrictions, and credential issues
Fastapi-Specific Remediation
Fixing CORS wildcard vulnerabilities in FastAPI requires a security-first approach. Here are specific remediation strategies:
1. Whitelist Specific Origins:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Define allowed origins explicitly
ALLOWED_ORIGINS = [
"https://yourdomain.com",
"https://app.yourdomain.com",
"https://admin.yourdomain.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "Content-Type", "X-Requested-With"],
)
2. Dynamic Origin Validation:
class DynamicCORSMiddleware(CORSMiddleware):
def __init__(self, app, allow_origins=None, **kwargs):
super().__init__(app, allow_origins, **kwargs)
self.allowed_origins = set(allow_origins or [])
async def __call__(self, request: Request, call_next):
origin = request.headers.get("origin")
if origin and origin in self.allowed_origins:
request.state.cors_origin = origin
return await super().__call__(request, call_next)
# Usage
app.add_middleware(
DynamicCORSMiddleware,
allow_origins=["https://yourdomain.com"],
allow_credentials=True,
)
3. Environment-Based Configuration:
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Load from environment with default
ALLOWED_ORIGINS = os.getenv(
"CORS_ALLOWED_ORIGINS",
"https://yourdomain.com,https://app.yourdomain.com"
).split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "Content-Type", "X-Requested-With"],
)
4. Security Headers Integration:
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware import Middleware
class SecurityMiddleware:
async def __call__(self, request: Request, call_next):
response = await call_next(request)
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-XSS-Protection"] = "1; mode=block"
return response
app = FastAPI(
middleware=[
Middleware(CORSMiddleware, allow_origins=["https://yourdomain.com"]),
Middleware(SecurityMiddleware),
]
)
5. Testing Your Fix:
import httpx
def test_cors_configuration():
origins = ["https://yourdomain.com", "https://evil.com"]
for origin in origins:
response = httpx.options(
"https://api.yourdomain.com/endpoint",
headers={
"Origin": origin,
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "X-Requested-With"
}
)
if origin == "https://yourdomain.com":
assert response.status_code == 200
assert response.headers["access-control-allow-origin"] == origin
else:
assert response.status_code == 403
Related CWEs: dataExposure
| CWE ID | Name | Severity |
|---|---|---|
| CWE-200 | Exposure of Sensitive Information | HIGH |
| CWE-209 | Error Information Disclosure | MEDIUM |
| CWE-213 | Exposure of Sensitive Information Due to Incompatible Policies | HIGH |
| CWE-215 | Insertion of Sensitive Information Into Debugging Code | MEDIUM |
| CWE-312 | Cleartext Storage of Sensitive Information | HIGH |
| CWE-359 | Exposure of Private Personal Information (PII) | HIGH |
| CWE-522 | Insufficiently Protected Credentials | CRITICAL |
| CWE-532 | Insertion of Sensitive Information into Log File | MEDIUM |
| CWE-538 | Insertion of Sensitive Information into Externally-Accessible File | HIGH |
| CWE-540 | Inclusion of Sensitive Information in Source Code | HIGH |