npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
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
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\-\.]+$' # No special chars for injection prevention
description:
type: string
maxLength: 2000
# Sanitize HTML entities
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 # CRITICAL: Prevents mass assignment
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 # Prevents data leakage of internal fields
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 ordersServer-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()
# Strict Pydantic models with security constraints
class ProductCreate(BaseModel):
model_config = ConfigDict(extra='forbid') # Reject unknown fields (mass assignment)
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):
# Prevent XSS via HTML entities
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
# Strip potential SQL injection patterns
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)
# Only validate JSON responses
content_type = response.headers.get('content-type', '')
if 'application/json' not in content_type:
return response
# Check if endpoint has a registered response schema
path = request.url.path
method = request.method
route_config = self.SCHEMA_MAP.get(path, {}).get(method)
if not route_config:
return response
# Read and validate response body
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:
# Log the validation failure for security monitoring
print(f"SECURITY: Response schema violation on {method} {path}: {e}")
# Return a safe error instead of potentially leaked data
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):
# ProductCreate model with extra='forbid' automatically rejects
# any unknown fields, preventing mass assignment attacks
# (e.g., attacker trying to set is_admin=true or price=0)
passCloudflare API Shield Schema Validation
# Upload OpenAPI schema to Cloudflare API Shield
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"
# Enable schema validation with blocking mode
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
# GitHub Actions workflow for schema validation in CI
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.jsSecurity 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
- OpenAPI Specification v3.1: https://spec.openapis.org/oas/v3.1.0
- Cloudflare API Shield Schema Validation: https://developers.cloudflare.com/api-shield/security/schema-validation/
- Redocly API Security by Design: https://redocly.com/learn/security
- Impart Security API Validation: https://www.impart.ai/blog/detect-and-fix-api-vulnerabilities-using-validation-secure-principles-and-real-time-response
- OWASP API Security Top 10 2023: https://owasp.org/API-Security/editions/2023/en/0x00-header/
References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 1
api-reference.md1.5 KB
API Reference: Implementing API Schema Validation Security
jsonschema (Python)
import jsonschema
schema = {
"type": "object",
"properties": {
"name": {"type": "string", "maxLength": 100},
"email": {"type": "string", "format": "email"},
},
"required": ["name", "email"],
"additionalProperties": False, # Prevent mass assignment
}
jsonschema.validate(instance=payload, schema=schema)OpenAPI Security Checks
| Check | Risk | Severity |
|---|---|---|
| No request body schema | Injection | HIGH |
| additionalProperties: true | Mass assignment | MEDIUM |
| String without maxLength | Buffer overflow | MEDIUM |
| No response schema | Data exposure | MEDIUM |
| No security scheme | Broken auth | CRITICAL |
| Security explicitly disabled | Unauthenticated access | CRITICAL |
OpenAPI Schema Best Practices
components:
schemas:
User:
type: object
additionalProperties: false
properties:
name:
type: string
maxLength: 100
pattern: "^[a-zA-Z ]+$"
email:
type: string
format: email
maxLength: 255
required: [name, email]Spectral (OpenAPI Linter)
spectral lint openapi.yaml --ruleset .spectral.yaml
# Custom security rules in .spectral.yamlReferences
- jsonschema: https://python-jsonschema.readthedocs.io/
- OpenAPI 3.0: https://spec.openapis.org/oas/v3.0.3
- Spectral: https://stoplight.io/open-source/spectral
Scripts 1
agent.py6.9 KB
#!/usr/bin/env python3
"""Agent for auditing API schema validation security using OpenAPI specs."""
import json
import argparse
from datetime import datetime
try:
import yaml
except ImportError:
yaml = None
try:
import jsonschema
except ImportError:
jsonschema = None
def load_openapi_spec(spec_path):
"""Load an OpenAPI specification file."""
with open(spec_path) as f:
if spec_path.endswith((".yaml", ".yml")):
return yaml.safe_load(f)
return json.load(f)
def audit_schema_validation(spec):
"""Audit OpenAPI spec for missing schema validation."""
findings = []
paths = spec.get("paths", {})
for path, methods in paths.items():
for method, details in methods.items():
if method in ("get", "post", "put", "patch", "delete"):
request_body = details.get("requestBody", {})
if method in ("post", "put", "patch") and not request_body:
findings.append({
"path": path, "method": method.upper(),
"issue": "no_request_body_schema", "severity": "HIGH",
})
elif request_body:
content = request_body.get("content", {})
for media, media_def in content.items():
schema = media_def.get("schema", {})
if not schema:
findings.append({
"path": path, "method": method.upper(),
"issue": "empty_request_schema", "severity": "HIGH",
})
elif schema.get("additionalProperties") is not False:
findings.append({
"path": path, "method": method.upper(),
"issue": "additional_properties_allowed",
"severity": "MEDIUM",
"risk": "mass_assignment",
})
params = details.get("parameters", [])
for param in params:
if not param.get("schema"):
findings.append({
"path": path, "method": method.upper(),
"parameter": param.get("name"),
"issue": "parameter_no_schema", "severity": "MEDIUM",
})
elif param.get("schema", {}).get("type") == "string":
schema = param["schema"]
if not schema.get("maxLength") and not schema.get("pattern"):
findings.append({
"path": path, "method": method.upper(),
"parameter": param.get("name"),
"issue": "string_no_max_length", "severity": "MEDIUM",
"risk": "injection",
})
responses = details.get("responses", {})
for code, resp in responses.items():
content = resp.get("content", {})
for media, media_def in content.items():
schema = media_def.get("schema", {})
if not schema:
findings.append({
"path": path, "method": method.upper(),
"response_code": code,
"issue": "no_response_schema", "severity": "MEDIUM",
"risk": "data_exposure",
})
return findings
def check_security_definitions(spec):
"""Check security scheme definitions in OpenAPI spec."""
findings = []
version = spec.get("openapi", spec.get("swagger", ""))
if version.startswith("3"):
security_schemes = spec.get("components", {}).get("securitySchemes", {})
else:
security_schemes = spec.get("securityDefinitions", {})
if not security_schemes:
findings.append({"issue": "no_security_schemes_defined", "severity": "CRITICAL"})
global_security = spec.get("security", [])
if not global_security:
findings.append({"issue": "no_global_security", "severity": "HIGH"})
paths = spec.get("paths", {})
for path, methods in paths.items():
for method, details in methods.items():
if method in ("get", "post", "put", "patch", "delete"):
op_security = details.get("security")
if op_security == []:
findings.append({
"path": path, "method": method.upper(),
"issue": "security_explicitly_disabled", "severity": "CRITICAL",
})
return findings
def validate_request_payload(schema_path, payload_path):
"""Validate a request payload against a JSON schema."""
if jsonschema is None:
return {"error": "jsonschema not installed"}
with open(schema_path) as f:
schema = json.load(f)
with open(payload_path) as f:
payload = json.load(f)
errors = []
v = jsonschema.Draft7Validator(schema)
for error in v.iter_errors(payload):
errors.append({
"path": list(error.path),
"message": error.message,
"schema_path": list(error.schema_path),
})
return {"valid": len(errors) == 0, "errors": errors}
def main():
parser = argparse.ArgumentParser(description="API Schema Validation Security Agent")
parser.add_argument("--spec", help="OpenAPI spec file (JSON/YAML)")
parser.add_argument("--schema", help="JSON Schema for validation")
parser.add_argument("--payload", help="Request payload to validate")
parser.add_argument("--output", default="schema_validation_report.json")
parser.add_argument("--action", choices=["audit", "security", "validate", "full"],
default="full")
args = parser.parse_args()
report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}}
if args.spec:
spec = load_openapi_spec(args.spec)
if args.action in ("audit", "full"):
f = audit_schema_validation(spec)
report["findings"]["schema_audit"] = f
print(f"[+] Schema validation issues: {len(f)}")
if args.action in ("security", "full"):
f = check_security_definitions(spec)
report["findings"]["security_schemes"] = f
print(f"[+] Security definition issues: {len(f)}")
if args.action == "validate" and args.schema and args.payload:
result = validate_request_payload(args.schema, args.payload)
report["findings"]["validation"] = result
print(f"[+] Validation: {'PASS' if result.get('valid') else 'FAIL'}")
with open(args.output, "w") as fout:
json.dump(report, fout, indent=2, default=str)
print(f"[+] Report saved to {args.output}")
if __name__ == "__main__":
main()