Fastapi Security Review

draft statusCoding & DevelopmentWasu6/18/2026
Export as Markdown

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.

Triggerssecurity review
0 copies0 worked0% successv1 version0 feedback

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 ครับ รับได้หลายรูปแบบ:

  1. Paste โค้ด ลงในช่องแชทโดยตรง
  2. อัปโหลดไฟล์ .py หรือ .zip ของทั้ง project
  3. วาง 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 HS256 or RS256 — not "none" or unset
  • JWT: exp claim validated; tokens not accepted after expiry
  • JWT: secret key is not hardcoded, not a default value ("secret", "changeme")
  • Password hashing uses bcrypt or argon2 — not md5, 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 pass params={} 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
  • subprocess always 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() or request.json() without validation
  • Pydantic fields use constr, conint, EmailStr, HttpUrl where appropriate
  • String fields have max_length set (prevent oversized payloads)
  • File uploads: content_type whitelisted, 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() or logger.debug() logging sensitive values
  • Error responses do not expose stack traces, SQL errors, or internal paths
  • HTTPException detail messages are user-safe (no DB error strings)
  • .env file in .gitignore; no secrets committed to repo
  • SECRET_KEY, DATABASE_URL loaded 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:

  • CORSMiddleware not using allow_origins=["*"] in production
  • allow_credentials=True never combined with allow_origins=["*"] (browser blocks + security risk)
  • Trusted origins list is explicit and reviewed
  • Security headers added (manually or via secure library):
    • X-Content-Type-Options: nosniff
    • X-Frame-Options: DENY
    • Strict-Transport-Security (HSTS)
    • Content-Security-Policy
    • Referrer-Policy: no-referrer
  • TrustedHostMiddleware configured 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)
  • slowapi or equivalent middleware present
  • No unbounded query results — all list endpoints use limit/offset or pagination
  • File upload size limits enforced (see Category 3)
  • No synchronous blocking calls inside async def routes (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.txt or pyproject.toml present with pinned versions
  • No known-vulnerable packages (check against common CVEs):
PackageRisky version rangeIssue
python-jose< 3.3.0Algorithm confusion attack
pyjwt< 2.4.0none algorithm bypass
starlette< 0.27.0Path traversal via static files
fastapi< 0.95.0Depends() bypass edge cases
sqlalchemy< 1.4.0Various injection edge cases
cryptography< 41.0.0Multiple CVEs
pillow< 10.0.1Image bomb / arbitrary code
requests< 2.31.0Proxy auth leak
  • Dev dependencies (pytest, black) not in production requirements.txt
  • No pip install of 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 HTTPException handler for consistent error format
  • No sensitive data in log messages (tokens, passwords, PII)
  • Logs include request ID / correlation ID for traceability
  • 422 Unprocessable Entity responses 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.BaseSettings used for config (not raw os.environ.get)
  • SECRET_KEY min 32 bytes, generated with secrets.token_hex(32)
  • Database credentials not in source code
  • Debug mode (app = FastAPI(debug=True)) disabled in production
  • reload=True in uvicorn.run() not used in production
  • Docker: secrets not passed as ENV in Dockerfile — use Docker secrets or .env at runtime
  • No .env file 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/limit have 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

SeverityLabelMeaning
🔴 CRITCriticalExploitable immediately, data breach or takeover risk
🟠 HIGHHighSignificant risk, should fix before production
🟡 MEDMediumExploitable under certain conditions
🔵 LOWLowBest practice gap, low direct risk
⚪ INFOInfoObservation, 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):

  1. [CRIT-01] JWT algorithm validation
  2. [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)   |