"""API Schema Validation Middleware for FastAPI
Enforces strict schema validation on all request and response payloads
to prevent injection, mass assignment, and data leakage attacks.
"""
from fastapi import FastAPI, Request, Response, HTTPException
from fastapi.middleware import Middleware
from pydantic import BaseModel, Field, field_validator, ConfigDict
from typing import List, Optional
import re
import json
from starlette.middleware.base import BaseHTTPMiddleware
app = FastAPI()
class ProductCreate(BaseModel):
model_config = ConfigDict(extra='forbid')
name: str = Field(min_length=1, max_length=200, pattern=r'^[a-zA-Z0-9\s\-\.]+$')
description: Optional[str] = Field(default=None, max_length=2000)
price: float = Field(gt=0, le=999999.99)
category: str = Field(pattern=r'^(electronics|clothing|food|furniture|other)$')
tags: Optional[List[str]] = Field(default=None, max_length=10)
@field_validator('name')
@classmethod
def sanitize_name(cls, v):
dangerous_patterns = ['<script', 'javascript:', 'onerror=', 'onload=']
lower_v = v.lower()
for pattern in dangerous_patterns:
if pattern in lower_v:
raise ValueError(f'Invalid characters in name')
return v
@field_validator('description')
@classmethod
def sanitize_description(cls, v):
if v is None:
return v
sql_patterns = [
r"('|--|;|/\*|\*/|xp_|exec\s|union\s+select|drop\s+table)",
]
for pattern in sql_patterns:
if re.search(pattern, v, re.IGNORECASE):
raise ValueError('Invalid content in description')
return v
@field_validator('tags')
@classmethod
def validate_tags(cls, v):
if v is None:
return v
if len(v) > 10:
raise ValueError('Maximum 10 tags allowed')
for tag in v:
if not re.match(r'^[a-zA-Z0-9\-]+$', tag) or len(tag) > 50:
raise ValueError(f'Invalid tag format: {tag}')
return v
class ProductResponse(BaseModel):
"""Response model that explicitly defines allowed output fields.
Prevents leakage of internal fields like internal_notes, cost_price, etc."""
model_config = ConfigDict(extra='forbid')
id: str
name: str
price: float
category: str
tags: List[str] = []
created_at: str
class ResponseValidationMiddleware(BaseHTTPMiddleware):
"""Middleware to validate response payloads against schema.
Prevents accidental data leakage by checking response content."""
SCHEMA_MAP = {
'/api/v2/products': {
'POST': {'response_model': ProductResponse},
'GET': {'response_model': ProductResponse},
}
}
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
content_type = response.headers.get('content-type', '')
if 'application/json' not in content_type:
return response
path = request.url.path
method = request.method
route_config = self.SCHEMA_MAP.get(path, {}).get(method)
if not route_config:
return response
body = b""
async for chunk in response.body_iterator:
body += chunk
try:
data = json.loads(body)
model = route_config['response_model']
if isinstance(data, list):
for item in data:
model.model_validate(item)
else:
model.model_validate(data)
except Exception as e:
print(f"SECURITY: Response schema violation on {method} {path}: {e}")
return Response(
content=json.dumps({"error": "Internal server error"}),
status_code=500,
media_type="application/json"
)
return Response(
content=body,
status_code=response.status_code,
headers=dict(response.headers),
media_type=response.media_type
)
app.add_middleware(ResponseValidationMiddleware)
@app.post("/api/v2/products", response_model=ProductResponse, status_code=201)
async def create_product(product: ProductCreate):
pass
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/api_gateway/user_schemas" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: multipart/form-data" \
-F "file=@openapi.yaml" \
-F "kind=openapi_v3"
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{zone_id}/api_gateway/settings/schema_validation" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"validation_default_mitigation_action": "block",
"validation_override_mitigation_action": null
}'
name: API Schema Security Check
on:
pull_request:
paths: ['api/**', 'openapi/**']
jobs:
schema-security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate OpenAPI Schema
run: |
npm install -g @stoplight/spectral-cli
spectral lint openapi.yaml --ruleset .spectral-security.yaml
- name: Check for Security Anti-Patterns
run: |
python3 scripts/schema_security_check.py openapi.yaml
- name: Run Contract Tests
run: |
npm install -g dredd
dredd openapi.yaml http://localhost:3000 --hookfiles=./test/hooks.js