Validate external input at API boundary only:
- Use Pydantic models for all request payloads
- Validation happens automatically in route parameters
- Trust internal code - don't re-validate
# API Layer - automatic validation
@router.post("/", response_model=Item)
def create_item(data: ItemCreate): # Pydantic validates here
return item_service.create_item(data)Use Pydantic field validators for sanitization:
from pydantic import BaseModel, field_validator
from app.core.security import sanitize_string
class ItemCreate(BaseModel):
name: str
@field_validator("name")
@classmethod
def sanitize_name(cls, v: str) -> str:
return sanitize_string(v, max_length=200)All sanitization functions in app/core/security.py.
sanitize_string(value, max_length):
- Remove control characters
- Trim whitespace
- Enforce length limits
validate_slug(value):
- Ensure URL-safe (lowercase, numbers, hyphens only)
- No leading/trailing hyphens
- No consecutive hyphens
# In Pydantic model
@field_validator("description")
@classmethod
def sanitize_description(cls, v: str | None) -> str | None:
if v is None:
return None
return sanitize_string(v, max_length=1000).envfiles in.gitignore- No hardcoded credentials
- No API keys in code
- No passwords in version control
Use Pydantic Settings:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str = "sqlite:///./test.db"
secret_key: str
class Config:
env_file = ".env"
settings = Settings()Pass as environment variables at runtime:
podman run -e SECRET_KEY=xxx myappDon't overrotate - apply security proportionally:
- SQL Injection: Not applicable (in-memory storage)
- XSS: Sanitize strings, FastAPI encodes JSON automatically
- Command Injection: Validate file paths, no shell execution
- Secrets Exposure: Environment variables only
- Broken Access Control: Not in reference (show pattern if needed)
String fields:
- Max length limits
- Character allowlist (for slugs)
- Trim whitespace
- Remove control characters
Numeric fields:
- Min/max validation
- Type checking (Pydantic automatic)
Examples:
# String with length limit
name: str = Field(..., min_length=1, max_length=200)
# Slug with pattern
slug: str = Field(..., pattern=r"^[a-z0-9-]+$")
# Optional with default
description: str | None = Field(None, max_length=1000)# Minimal base image
FROM python:3.11-slim
# Non-root user
RUN useradd -m -u 1000 app
USER app
# Copy with ownership
COPY --chown=app:app . /app
# No secrets in layers
# Use runtime env vars insteadHEALTHCHECK --interval=30s --timeout=3s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"# Run Bandit for security issues
bandit -r app/
# Check dependencies for vulnerabilities
safety checkBoth run in .github/workflows/security.yml:
- Weekly schedule
- On push/PR
- Fail build on HIGH severity
DON'T:
- ❌ Validate the same data multiple times
- ❌ Sanitize in both model and service layer
- ❌ Trust user input without validation
- ❌ Commit
.envfiles - ❌ Hardcode secrets
DO:
- ✅ Validate once at API boundary
- ✅ Use Pydantic validators
- ✅ Keep secrets in environment
- ✅ Run security scans in CI
- ✅ Keep approach proportional (light touch)