Phishing Api Keys in Fastapi
How Phishing API Keys Manifests in FastAPI
In FastAPI applications, phishing API keys typically refers to the accidental exposure of valid API keys in responses, logs, or error messages that an attacker can harvest. This is a specific instance of sensitive data exposure (OWASP API Security Top 10 API6:2023). FastAPI's automatic OpenAPI generation and detailed error handling, while developer-friendly, can inadvertently leak keys if not carefully configured.
Common FastAPI-specific attack patterns:
- Hardcoded keys in route handlers: Developers sometimes embed third-party service API keys (e.g., Stripe, Google Maps, OpenAI) directly in endpoint code for convenience. If an exception occurs, FastAPI's default debug mode may return the full traceback in the response, exposing the key.
- Leakage via OpenAPI schema: FastAPI auto-generates OpenAPI specs. If an API key is used as a parameter default (e.g.,
api_key: str = "sk-..."), it may appear in theexampleordefaultfields of the generated schema, which is publicly accessible at/openapi.json. - Logging sensitive headers: FastAPI's default logging or custom middleware might log entire request headers, including
Authorization: Bearer <key>or customX-API-Keyheaders. These logs are often stored in plaintext and accessible to broader teams or compromised systems. - Error messages with context: When using Pydantic models for request validation, FastAPI returns detailed validation errors. If a model field contains an API key and validation fails, the key might be included in the
detailresponse body.
Example vulnerable FastAPI code:
from fastapi import FastAPI, HTTPException
import os
app = FastAPI()
# VULNERABLE: Hardcoded key in route
@app.post("/send-email")
async def send_email(to: str, message: str):
# Attacker triggers an error; traceback reveals this key
sendgrid_key = "SG.xxxxx" # Hardcoded SendGrid API key
# ... logic using sendgrid_key ...
if not to:
raise HTTPException(status_code=400, detail="Missing 'to' field")
return {"status": "sent"}
# VULNERABLE: Key in OpenAPI schema example
from pydantic import BaseModel
class PaymentRequest(BaseModel):
stripe_key: str = "sk_live_xxxx" # Appears in /openapi.json as example
amount: int
@app.post("/charge")
async def charge(payment: PaymentRequest):
# ...
pass
In the first example, if to is missing, FastAPI's default error response includes the line where the key is defined in the traceback (when debug=True). In the second, the stripe_key default becomes part of the public OpenAPI schema, allowing anyone to harvest valid keys by inspecting /openapi.json.
FastAPI-Specific Detection
Detecting phishing API key exposure requires scanning both the runtime behavior (responses, logs) and the static OpenAPI specification. Manual checks include:
- Reviewing
/openapi.jsonfor anyexample,default, orenumvalues that resemble API keys (long alphanumeric strings, often with prefixes likesk-,AKIA,ghp_). - Testing endpoints with invalid/missing parameters to see if error responses leak sensitive data.
- Inspecting application logs for occurrences of
AuthorizationorX-API-Keyheaders.
Automated detection with middleBrick:
middleBrick's Data Exposure check (one of its 12 parallel security tests) automatically hunts for exposed secrets in API responses and OpenAPI specs. When you scan a FastAPI endpoint:
- middleBrick fetches the live
/openapi.json(if available) and analyzes all schema definitions, parameters, and examples for patterns matching known API key formats using 27+ regex patterns. - It sends crafted requests to trigger error conditions (e.g., missing required fields, invalid types) and inspects the response bodies and headers for leaked keys.
- It cross-references any found secrets with the OpenAPI spec to determine if they are static defaults (hardcoded) or dynamic values that should be protected.
For example, scanning the vulnerable /charge endpoint above would yield a Data Exposure finding with high severity, pointing to the stripe_key default in the OpenAPI schema. The report includes the exact location (schema path) and a snippet of the exposed value.
You can run this scan instantly from the middleBrick web dashboard or via the CLI:
middlebrick scan https://your-fastapi-app.com/openapi.json
The CLI outputs JSON with a data_exposure category score and prioritized findings. In CI/CD, the GitHub Action can fail a build if any Data Exposure finding exceeds a threshold.
FastAPI-Specific Remediation
Remediation focuses on eliminating hardcoded secrets, securing error responses, and protecting logs. Use FastAPI's native features and Python best practices:
1. Never hardcode keys; use environment variables and Pydantic Settings.
Store secrets in environment variables (or a secrets manager) and load them via pydantic-settings. FastAPI integrates seamlessly.
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
sendgrid_key: str # Loaded from env var SENDGRID_KEY
stripe_key: str
model_config = SettingsConfigDict(env_file=".env")
settings = Settings()
@app.post("/send-email")
async def send_email(to: str, message: str):
# Use settings.sendgrid_key — never hardcoded
# ...
pass
Ensure .env is in .gitignore. In production, inject secrets via the environment (Docker, Kubernetes secrets, etc.).
2. Remove defaults from Pydantic models that contain secrets.
Do not set API key fields with default values in request/response models. If a field is required, leave it without a default. For optional fields, use None.
class PaymentRequest(BaseModel):
stripe_key: str # Required, no default — won't leak in OpenAPI
amount: int
# Alternatively, for headers:
from fastapi import Header
@app.post("/charge")
async def charge(
stripe_key: str = Header(..., alias="X-Stripe-Key") # No default
):
# ...
pass
3. Sanitize error responses and disable debug mode in production.
Never run FastAPI with debug=True in production. Configure custom exception handlers to avoid leaking internal data.
from fastapi import Request
from fastapi.responses import JSONResponse
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
# Return minimal detail; never include exc.detail if it might contain secrets
return JSONResponse(
status_code=exc.status_code,
content={"error": "Invalid request"}, # Generic message
)
# In production, set debug=False when instantiating FastAPI
app = FastAPI(debug=False)
4. Filter sensitive headers from logs.
Use a logging filter to redact Authorization, X-API-Key, and similar headers.
import logging
class SensitiveHeaderFilter(logging.Filter):
def filter(self, record):
# Assuming log record has request headers in record.args or record.msg
# This is a simplified example; adapt to your logging setup
if hasattr(record, "request"):
headers = record.request.headers
headers["authorization"] = "[REDACTED]"
headers["x-api-key"] = "[REDACTED]"
return True
logging.getLogger("uvicorn.access").addFilter(SensitiveHeaderFilter())
5. Validate and monitor your OpenAPI spec.
Periodically inspect /openapi.json for any lingering secrets. You can also configure FastAPI to exclude certain routes from the schema using include_in_schema=False.
@app.post("/internal/secret-endpoint", include_in_schema=False)
async def internal_secret():
# Won't appear in OpenAPI
pass
After applying these fixes, re-scan with middleBrick to verify the Data Exposure score improves. The remediation guidance in middleBrick's report for this finding will list these exact FastAPI patterns.
Frequently Asked Questions
How does middleBrick detect phishing API keys in a FastAPI app?
sk_live_...), AWS (AKIA...), GitHub (ghp_...), and generic alphanumeric tokens. The scan takes 5–15 seconds and requires no credentials.What's the most common FastAPI mistake that leads to API key phishing?
stripe_key: str = "sk_live_xxxx"). This exposes the key in the automatically generated /openapi.json, which is publicly accessible. Always remove defaults for sensitive fields and load keys from environment variables at runtime.