Fastapi Security Review
Perform a security-focused code review for Python FastAPI projects. Trigger phrase: 'security review', 'review my code', 'check security', 'audit this code'. Covers OWASP Top 10, FastAPI-specific vulnerabilities, dependency scanning, and produces a structured findings report with severity ratings and fix recommendations. No external tools required — analysis is done by Claude reading the code directly.
Version 1
FastAPI Security Review — Skill
Triggers
- "security review" / "review my code" / "audit this code" / "check security" → ask for code input if not already provided, then run full review
Step 0 — Collect input (ALWAYS do this first)
Check whether code is already present in context (pasted inline, uploaded file, or prior message).
If no code is present, ask:
"กรุณาแชร์โค้ดที่ต้องการ review ครับ รับได้หลายรูปแบบ:
- Paste โค้ด ลงในช่องแชทโดยตรง
- อัปโหลดไฟล์ .py หรือ .zip ของทั้ง project
- วาง GitHub URL ของ repo หรือ file ที่ต้องการ"
If code is present, confirm scope before starting:
"จะ review ไฟล์ทั้งหมดที่ให้มานะครับ รวมถึง:
- [list detected files or modules] มีส่วนไหนที่อยากให้เน้นเป็นพิเศษไหมครับ? (เช่น auth, database, API endpoints)"
Step 1 — Inventory & map the codebase
Before checking vulnerabilities, build a mental map of the project:
Identify:
- Entry point: where app = FastAPI() is defined
- Routers: all APIRouter instances and included routes
- Auth mechanism: OAuth2, JWT, API Key, session, or none
- Database layer: SQLAlchemy, Tortoise ORM, raw SQL, or none
- External calls: httpx, requests, boto3, etc.
- Config/secrets: .env, pydantic Settings, os.environ usage
- Middleware: CORSMiddleware, TrustedHostMiddleware, custom
- File handling: UploadFile, open(), file paths
- Background tasks: BackgroundTasks, Celery, APScheduler
Print a brief Codebase Map before findings:
📁 Codebase Map
├── Entry point : main.py (app = FastAPI())
├── Routers : auth.py, users.py, items.py
├── Auth : JWT (python-jose) + OAuth2PasswordBearer
├── Database : SQLAlchemy + PostgreSQL
├── Config : pydantic BaseSettings (.env)
├── Middleware : CORSMiddleware
└── File upload : UploadFile (detected in /items)
Step 2 — Run security checks across 10 categories
For each category below, scan every relevant code section and collect findings. Each finding must include: severity, location (file + line if known), description, evidence (code snippet), and fix.
Category 1 — Authentication & Authorization
Checks:
- All sensitive endpoints have a
Depends(get_current_user)or equivalent guard - No endpoint accidentally missing auth dependency (especially after router refactoring)
- JWT: algorithm explicitly set to
HS256orRS256— not"none"or unset - JWT:
expclaim validated; tokens not accepted after expiry - JWT: secret key is not hardcoded, not a default value (
"secret","changeme") - Password hashing uses
bcryptorargon2— notmd5,sha1, or plain text -
OAuth2PasswordBearer(tokenUrl=...)scope properly enforced - Role-based access: admin routes not accessible by regular users
- Password reset tokens: single-use, time-limited, stored as hash
FastAPI-specific pitfalls:
# ❌ DANGEROUS — dependency in router but not enforced per-route
router = APIRouter(prefix="/admin") # no dependencies= here
@router.get("/users") # no Depends() here either
async def get_all_users(): ...
# ✅ CORRECT
router = APIRouter(prefix="/admin", dependencies=[Depends(require_admin)])
Category 2 — Injection Vulnerabilities
SQL Injection:
- No f-string or
.format()string building in SQL queries - All queries use ORM methods or parameterised queries (
text()with:param) -
execute(text(...))calls always passparams={}separately
# ❌ DANGEROUS
query = f"SELECT * FROM users WHERE email = '{email}'"
db.execute(query)
# ✅ CORRECT
db.execute(text("SELECT * FROM users WHERE email = :email"), {"email": email})
Command Injection:
- No
os.system(),subprocess.run(shell=True)with user input -
subprocessalways uses list form:["cmd", arg1, arg2]
Template/Expression Injection:
- No
eval(),exec(),compile()on user-supplied strings - Jinja2 templates use
autoescape=True
Category 3 — Input Validation & Pydantic Models
Checks:
- All request bodies use Pydantic models — no raw
request.body()orrequest.json()without validation - Pydantic fields use
constr,conint,EmailStr,HttpUrlwhere appropriate - String fields have
max_lengthset (prevent oversized payloads) - File uploads:
content_typewhitelisted, file size limited - Path parameters validated — no directory traversal via
../ - No
orm_mode = True(v1) /from_attributes = True(v2) exposing unintended fields - Response models defined — not returning raw DB objects
# ❌ DANGEROUS — no size/type restriction
@router.post("/upload")
async def upload(file: UploadFile):
content = await file.read() # could be gigabytes
# ✅ CORRECT
MAX_SIZE = 5 * 1024 * 1024 # 5 MB
ALLOWED_TYPES = {"image/jpeg", "image/png", "application/pdf"}
@router.post("/upload")
async def upload(file: UploadFile):
if file.content_type not in ALLOWED_TYPES:
raise HTTPException(400, "File type not allowed")
content = await file.read(MAX_SIZE + 1)
if len(content) > MAX_SIZE:
raise HTTPException(413, "File too large")
Category 4 — Sensitive Data Exposure
Checks:
- Passwords, tokens, card numbers never returned in API responses
- Response models explicitly exclude sensitive fields (
password,hashed_password,secret) - No
print()orlogger.debug()logging sensitive values - Error responses do not expose stack traces, SQL errors, or internal paths
-
HTTPExceptiondetail messages are user-safe (no DB error strings) -
.envfile in.gitignore; no secrets committed to repo -
SECRET_KEY,DATABASE_URLloaded from environment — not hardcoded
# ❌ DANGEROUS — leaks hash in response
class UserResponse(BaseModel):
id: int
email: str
hashed_password: str # never expose this
# ✅ CORRECT
class UserResponse(BaseModel):
id: int
email: str
model_config = ConfigDict(from_attributes=True)
Category 5 — CORS & Security Headers
Checks:
-
CORSMiddlewarenot usingallow_origins=["*"]in production -
allow_credentials=Truenever combined withallow_origins=["*"](browser blocks + security risk) - Trusted origins list is explicit and reviewed
- Security headers added (manually or via
securelibrary):X-Content-Type-Options: nosniffX-Frame-Options: DENYStrict-Transport-Security(HSTS)Content-Security-PolicyReferrer-Policy: no-referrer
-
TrustedHostMiddlewareconfigured if app is behind a proxy
# ❌ DANGEROUS
app.add_middleware(CORSMiddleware,
allow_origins=["*"],
allow_credentials=True) # browsers will reject this AND it's a security risk
# ✅ CORRECT
app.add_middleware(CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["Authorization", "Content-Type"])
Category 6 — Rate Limiting & Denial of Service
Checks:
- Rate limiting applied to auth endpoints (
/login,/token,/register,/password-reset) -
slowapior equivalent middleware present - No unbounded query results — all list endpoints use
limit/offsetor pagination - File upload size limits enforced (see Category 3)
- No synchronous blocking calls inside
async defroutes (time.sleep,requests.get, heavy CPU) - Background tasks used for long-running work, not blocking the event loop
# ❌ DANGEROUS — blocks entire event loop
@router.get("/report")
async def get_report():
time.sleep(10) # blocks all other requests
return generate_report()
# ✅ CORRECT — offload to thread pool
from fastapi.concurrency import run_in_threadpool
@router.get("/report")
async def get_report():
result = await run_in_threadpool(generate_report)
return result
Category 7 — Dependency & Library Security
Checks:
-
requirements.txtorpyproject.tomlpresent with pinned versions - No known-vulnerable packages (check against common CVEs):
| Package | Risky version range | Issue |
|---|---|---|
python-jose | < 3.3.0 | Algorithm confusion attack |
pyjwt | < 2.4.0 | none algorithm bypass |
starlette | < 0.27.0 | Path traversal via static files |
fastapi | < 0.95.0 | Depends() bypass edge cases |
sqlalchemy | < 1.4.0 | Various injection edge cases |
cryptography | < 41.0.0 | Multiple CVEs |
pillow | < 10.0.1 | Image bomb / arbitrary code |
requests | < 2.31.0 | Proxy auth leak |
- Dev dependencies (
pytest,black) not in productionrequirements.txt - No
pip installof packages from untrusted URLs or local paths
Category 8 — Error Handling & Logging
Checks:
- Global exception handler registered — unhandled errors return 500, not stack trace
- Custom
HTTPExceptionhandler for consistent error format - No sensitive data in log messages (tokens, passwords, PII)
- Logs include request ID / correlation ID for traceability
-
422 Unprocessable Entityresponses do not expose internal model structure beyond necessary
# ✅ CORRECT — global handler
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
logger.error(f"Unhandled error: {exc}", exc_info=True) # log internally
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"} # safe for client
)
Category 9 — Configuration & Secrets Management
Checks:
-
pydantic_settings.BaseSettingsused for config (not rawos.environ.get) -
SECRET_KEYmin 32 bytes, generated withsecrets.token_hex(32) - Database credentials not in source code
- Debug mode (
app = FastAPI(debug=True)) disabled in production -
reload=Trueinuvicorn.run()not used in production - Docker: secrets not passed as ENV in
Dockerfile— use Docker secrets or.envat runtime - No
.envfile committed to git
# ✅ CORRECT config pattern
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
secret_key: str
database_url: str
debug: bool = False
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
settings = Settings()
Category 10 — Business Logic & API Design
Checks:
- IDOR (Insecure Direct Object Reference): resource ownership verified before returning/modifying
- Mass assignment: Pydantic models used for update — not
**request.dict()directly into ORM - Idempotency: POST endpoints protected against double-submit (idempotency key or DB unique constraint)
- Soft delete vs hard delete: deleted records not accessible via API
- Pagination:
skip/limithave upper bounds (limit: int = Query(20, le=100))
# ❌ DANGEROUS — IDOR: any authenticated user can fetch any order
@router.get("/orders/{order_id}")
async def get_order(order_id: int, db: Session = Depends(get_db),
current_user = Depends(get_current_user)):
return db.query(Order).filter(Order.id == order_id).first()
# ✅ CORRECT — ownership check
@router.get("/orders/{order_id}")
async def get_order(order_id: int, db: Session = Depends(get_db),
current_user = Depends(get_current_user)):
order = db.query(Order).filter(
Order.id == order_id,
Order.user_id == current_user.id # ownership enforced
).first()
if not order:
raise HTTPException(404, "Order not found")
return order
Step 3 — Compile findings report
After scanning all 10 categories, produce a structured report.
Severity levels
| Severity | Label | Meaning |
|---|---|---|
| 🔴 CRIT | Critical | Exploitable immediately, data breach or takeover risk |
| 🟠 HIGH | High | Significant risk, should fix before production |
| 🟡 MED | Medium | Exploitable under certain conditions |
| 🔵 LOW | Low | Best practice gap, low direct risk |
| ⚪ INFO | Info | Observation, no immediate risk |
Report format (print in chat)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔐 FastAPI Security Review Report
Generated: YYYY-MM-DD
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📁 Codebase Map
[... map from Step 1 ...]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 Summary
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔴 Critical : X
🟠 High : X
🟡 Medium : X
🔵 Low : X
⚪ Info : X
─────────────────
Total : X findings
Overall Risk : CRITICAL / HIGH / MEDIUM / LOW
(highest severity present)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔴 CRITICAL FINDINGS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[CRIT-01] JWT Algorithm Not Validated
Category : Authentication & Authorization
Location : auth/jwt.py, line 24
Description:
The JWT decode call does not specify allowed algorithms,
allowing an attacker to forge tokens using the "none" algorithm.
Evidence:
```python
payload = jwt.decode(token, SECRET_KEY) # ❌ no algorithms= arg
Fix:
payload = jwt.decode(
token, SECRET_KEY,
algorithms=["HS256"] # ✅ explicit allowlist
)
Impact : Full authentication bypass — any user can forge any token CVSS-like : 9.8 (Critical)
─────────────────────────────────────
[CRIT-02] ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🟠 HIGH FINDINGS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[HIGH-01] ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🟡 MEDIUM FINDINGS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[MED-01] ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🔵 LOW / ⚪ INFO ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[LOW-01] ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ✅ PASSED CHECKS (no issues found) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- Password hashing: bcrypt detected ✓
- Pydantic models on all endpoints ✓
- ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🛠 REMEDIATION PRIORITY ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Fix immediately (before any deployment):
- [CRIT-01] JWT algorithm validation
- [CRIT-02] ...
Fix this sprint: 3. [HIGH-01] ...
Fix next sprint: 5. [MED-01] ...
Backlog / best practice: 8. [LOW-01] ...
---
## Step 4 — Offer follow-up actions
After the report, always offer:
> "ต้องการให้ช่วยอะไรเพิ่มเติมไหมครับ?
> 1. **แก้โค้ด** — ให้เขียนโค้ดที่แก้ไขแล้วสำหรับ finding ที่เลือก
> 2. **Export report** — บันทึก report เป็นไฟล์ .md หรือ .txt
> 3. **Deep dive** — อธิบาย finding ใดเพิ่มเติม พร้อม exploit scenario
> 4. **Re-scan** — รับโค้ดที่แก้แล้วและยืนยันว่า fix ถูกต้อง"
---
## Reviewer mindset — principles to apply throughout
- **Assume attacker perspective**: for each endpoint, ask "what happens if I send unexpected input?"
- **Check defaults**: FastAPI/Starlette defaults are often permissive — explicit is safer
- **Follow the data**: trace user input from request → validation → business logic → DB → response
- **Dependency chain**: a secure endpoint can be broken by an insecure shared dependency
- **Context matters**: a `LOW` finding in a public API may be `HIGH` in an internal admin panel
- **Don't just find — fix**: every finding must include a concrete, copy-pasteable fix
## Reference: OWASP Top 10 mapping
| OWASP 2021 | Covered in category |
|---------------------|---------------------------|
| A01 Broken Access Control | Cat 1, Cat 10 |
| A02 Cryptographic Failures | Cat 4, Cat 9 |
| A03 Injection | Cat 2 |
| A04 Insecure Design | Cat 10 |
| A05 Security Misconfiguration | Cat 5, Cat 9 |
| A06 Vulnerable Components | Cat 7 |
| A07 Auth Failures | Cat 1 |
| A08 Software Integrity | Cat 7 |
| A09 Logging Failures | Cat 8 |
| A10 SSRF | Cat 2 (external calls) |