| name | implementing-api-schema-validation-security |
| description | Implement API schema validation using OpenAPI specifications and JSON Schema to enforce input/output contracts and prevent injection, data exposure, and mass assignment attacks. |
| domain | cybersecurity |
| subdomain | api-security |
| tags | ["api-security","schema-validation","openapi","json-schema","input-validation","data-leakage-prevention","mass-assignment","api-gateway"] |
| version | 1.0 |
| author | mahipal |
| license | Apache-2.0 |
| nist_csf | ["PR.PS-01","ID.RA-01","PR.DS-10","DE.CM-01"] |
Implementing API Schema Validation Security
Overview
API schema validation enforces that all data exchanged through APIs conforms to a predefined structure defined in OpenAPI Specification (OAS) or JSON Schema documents. This prevents injection attacks (SQLi, XSS, XXE), blocks mass assignment by rejecting unknown properties, prevents data leakage by validating response schemas, and ensures type safety across all API interactions. Schema validation operates at both the API gateway level (runtime enforcement) and during development (shift-left security).
When to Use
- When deploying or configuring implementing api schema validation security capabilities in your environment
- When establishing security controls aligned to compliance requirements
- When building or improving security architecture for this domain
- When conducting security assessments that require this implementation
Common Misconfigurations & Verification
additionalProperties missing or true: the single most common gap - without additionalProperties: false, mass-assignment fields pass validation; set it on every object schema.
- Request-only validation: validating input but not responses lets excessive-data-exposure through; validate/serialize responses with explicit DTOs.
- Loose constraints: strings without
maxLength/pattern and fields without enum permit injection and DoS payloads.
readOnly not enforced server-side: clients can still write readOnly fields if the server only documents them rather than rejecting them.
- Permissive/verbose errors: leaking schema detail in 400 responses aids attackers.
- Spec drift: the deployed schema lagging the live API leaves new endpoints unvalidated.
How to verify it works: send a request with an extra/unknown field and confirm 400 (not silent acceptance); submit over-long and pattern-violating values and confirm rejection; attempt to set a readOnly field and confirm it is ignored; force a sensitive field into a response and confirm response validation strips it or fails safely; diff the enforced spec against the live route list.
Prerequisites
- OpenAPI Specification v3.0 or v3.1 for all API endpoints
- API gateway with schema validation support (Cloudflare API Shield, Kong, AWS API Gateway)
- JSON Schema draft-07 or later understanding
- Development environment with OpenAPI validation libraries
- CI/CD pipeline for automated schema compliance testing
Core Implementation
OpenAPI Schema with Security Constraints
openapi: 3.1.0
info:
title: Secure E-Commerce API
version: 2.0.0
servers:
- url: https://api.example.com/v2
description: Production (HTTPS enforced)
security:
- OAuth2:
- read:products
- write:orders
paths:
/products:
post:
operationId: createProduct
security:
- OAuth2: [write:products]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProductCreate'
responses:
'201':
description: Product created
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
/products/{productId}:
get:
operationId: getProduct
parameters:
- name: productId
in: path
required: true
schema:
type: string
format: uuid
pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
components:
schemas:
ProductCreate:
type: object
required: [name, price, category]
properties:
name:
type: string
minLength: 1
maxLength: 200
pattern: '^[a-zA-Z0-9\s\-\.]+$'
description:
type: string
maxLength: 2000
price:
type: number
format: float
minimum: 0.01
maximum: 999999.99
exclusiveMinimum: 0
category:
type: string
enum: [electronics, clothing, food, furniture, other]
tags:
type: array
items:
type: string
maxLength: 50
pattern: '^[a-zA-Z0-9\-]+$'
maxItems: 10
uniqueItems: true
additionalProperties: false
Product:
type: object
required: [id, name, price]
properties:
id:
type: string
format: uuid
readOnly: true
name:
type: string
price:
type: number
category:
type: string
tags:
type: array
items:
type: string
createdAt:
type: string
format: date-time
readOnly: true
additionalProperties: false
ValidationErrorResponse:
type: object
required: [code, message]
properties:
code:
type: string
enum: [VALIDATION_ERROR]
message:
type: string
maxLength: 500
details:
type: array
items:
type: object
properties:
field:
type: string
error:
type: string
additionalProperties: false
maxItems: 50
additionalProperties: false
responses:
ValidationError:
description: Request validation failed
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationErrorResponse'
Unauthorized:
description: Authentication required
securitySchemes:
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.example.com/authorize
tokenUrl: https://auth.example.com/token
scopes:
read:products: Read product data
write:products: Create and update products
write:orders: Create orders
Server-Side Schema Validation (Python/FastAPI)
"""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
Cloudflare API Shield Schema Validation
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
}'
CI/CD Schema Compliance Testing
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
Security Anti-Patterns to Detect
| Anti-Pattern | Risk | Fix |
|---|
additionalProperties: true or missing | Mass assignment | Set additionalProperties: false |
No maxLength on strings | Buffer overflow, DoS | Add appropriate maxLength constraints |
No pattern on string fields | Injection attacks | Add regex patterns to restrict input |
No enum for fixed-value fields | Unexpected input processing | Use enum for fields with known values |
format: password without TLS | Credential exposure | Enforce HTTPS-only server URLs |
| Missing error response schemas | Information leakage | Define all 4xx/5xx response schemas |
readOnly fields in request body | Data manipulation | Enforce readOnly server-side |
References