Python API Security with OWASP
A secure Python API does not come from one middleware. The short answer is to authenticate callers, authorize every action and object, validate all input, expose only required browser origins, constrain abuse, keep secrets out of code, and observe the system without recording credentials. Those controls must span design, application code, infrastructure, and tests.
The OWASP API Security project organizes recurring risks, but it is not a checklist to open after deployment. Use it while threat modeling: what data exists, who may read it, how an attacker could automate requests, and what a stolen credential permits. The OWASP REST Security Cheat Sheet adds practical defensive guidance.
Authentication is not authorization
Authentication (authn) establishes identity. Authorization (authz) determines whether that identity may perform this operation on this resource. A valid token does not automatically grant access to GET /invoices/42. The server must establish that the invoice belongs to the caller or that an assigned role explicitly permits access. This object-level check prevents a common class of broken authorization.
Prefer established protocols, short-lived tokens, and narrow scopes. Verify signature, issuer, audience, expiry, and an explicitly configured algorithm server-side. Do not trust an algorithm selected by the untrusted token. For browser sessions, Secure, HttpOnly, and suitable SameSite cookies reduce exposure. The official FastAPI security documentation explains OAuth2 components; your application still owns its access policy.
Keep that policy close to resource retrieval:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from pydantic import BaseModel, ConfigDict
app = FastAPI()
class User(BaseModel):
id: int
is_admin: bool = False
class Invoice(BaseModel):
model_config = ConfigDict(extra="forbid")
id: int
owner_id: int
total_cents: int
def current_user() -> User:
# Validate a real credential and its claims in production.
return User(id=7)
def load_invoice(invoice_id: int) -> Invoice | None:
return Invoice(id=invoice_id, owner_id=7, total_cents=5900)
@app.get("/invoices/{invoice_id}")
def get_invoice(invoice_id: int, user: Annotated[User, Depends(current_user)]):
invoice = load_invoice(invoice_id)
if invoice is None:
raise HTTPException(status_code=404, detail="Invoice not found")
if invoice.owner_id != user.id and not user.is_admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
return invoice
Never use an owner_id from the request body as the source of ownership; derive it from authenticated identity. The FastAPI REST API guide covers the framework fundamentals.
Validate input and control output
Validate type, length, format, range, and business meaning. Rejecting unknown fields reduces mass assignment: callers cannot append is_admin=true to a permissive model. Limit total body size, collection length, upload size, and structural depth at the proxy or application server. Serializers define these contracts, as shown in the Django REST Framework guide, but SQL still requires parameters and file paths need dedicated containment rules.
Do not return tracebacks, SQL fragments, internal paths, or cryptographic details. Public errors can contain a stable description and a request_id; complete diagnostics belong in protected internal telemetry. The guide to Python exception handling helps avoid broad handlers that hide actionable failures.
CORS and rate limiting solve different problems
CORS tells browsers which origins may read responses. It is not authn, authz, or CSRF protection. List exact origins, required methods, and headers. Avoid reflecting arbitrary Origin values; wildcard policies combined with credentials are especially risky. Keep development and production policies separate.
Rate limiting reduces brute force, scraping, and accidental overuse, but it does not fix an inherently expensive endpoint. Assign quotas to an authenticated identity and, before login, to an IP or client key. Login and password recovery deserve tighter controls. A multi-instance service needs a shared counter and should return 429 with Retry-After. Add timeouts, pagination, upload ceilings, and concurrency budgets. Select numbers from observed legitimate traffic rather than copying a fashionable default.
Secrets, dependencies, and runtime configuration
Credentials do not belong in Git, a committed .env, logs, tracebacks, or images. In production, inject them from a secret manager, grant each workload the minimum access, and plan rotation. Refuse to start when required configuration is absent. If a key leaks, revoke it; deleting one commit cannot remove forks, caches, or build artifacts.
Pin and review dependencies, keep Python and libraries on supported releases, and scan them in CI. Isolated environments from the Python venv guide prevent dependency collisions but do not replace updates and inventory. Containers should run without root and carry only required files; the Python Docker guide explains that deployment boundary.
Useful logging without sensitive data
Record structured events with timestamp, normalized route, status, latency, server-generated request identifier, and a pseudonymous internal principal identifier. Do not log complete bodies, Authorization, cookies, passwords, or tokens by default. Redaction must occur before an event leaves the process. Define access, retention, and alerts for repeated login failures, unusual 403 or 429 rates, and administrative changes.
A server-generated request_id correlates proxy, application, and database events without personal data. Do not blindly accept a client value: constrain its format and length or generate a replacement. The Python logging tutorial covers levels and handlers without duplicate output.
Common mistakes
- Validating a token but omitting permission checks for individual objects.
- Treating CORS
*as an API security boundary. - Accepting unknown fields or trusting a client-supplied price, role, or owner.
- Applying one global limit that harms legitimate consumers and is easy to evade.
- Recording every header and body to simplify debugging.
- Keeping non-rotated keys or sharing one service credential everywhere.
- Returning distinguishable errors that reveal whether an email or resource exists.
Pre-release checklist
- Model resources, roles, scopes, and denial cases; test horizontal and vertical access.
- Validate token signatures and claims, expire credentials, and support revocation or rotation.
- Reject extra fields, constrain payloads, and parameterize database queries.
- Allow exact CORS origins and address CSRF when cookies authenticate requests.
- Enforce distributed limits, timeouts, pagination, and upload ceilings.
- Inject secrets outside artifacts and isolate access by workload.
- Redact sensitive log fields and protect telemetry retention.
- Automate
401,403,404,422, and429tests with pytest.
Security is a continuous property. Revisit the threat model when endpoints, integrations, or data classes change, monitor the resulting signals, and convert incidents and discoveries into repeatable tests and controls.