Auth, Config & Security Headers
Broken Access Control is the #1 OWASP Top 10 risk — found in 94% of applications. Authentication flaws, misconfigured CORS, and missing security headers are silent vulnerabilities that make exploitation trivial.
Authentication
Password Storage
| Algorithm | Status | Notes |
|---|---|---|
| Argon2id | Recommended | Memory-hard, best resistance to GPU attacks |
| bcrypt | Good | Proven, widely supported |
| scrypt | Good | Memory-hard alternative |
| MD5, SHA-1, SHA-256 | Never use | Too fast, no salt, trivially crackable |
# Python — Argon2id
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
hashed = ph.hash("user_password")
ph.verify(hashed, "user_password") # raises on mismatch
Session Security
| Requirement | Value |
|---|---|
| Token entropy | Minimum 128 bits |
| Absolute timeout | 24 hours |
| Idle timeout | 30 minutes (sensitive apps) |
| Cookie flags | httpOnly, secure, sameSite=Lax |
JWT Best Practices
| Check | Reason |
|---|---|
| Verify signature | Prevents token forgery |
Check iss and aud claims |
Prevents cross-service token reuse |
Enforce expiration (exp) |
Limits exposure window |
Reject alg: none |
Prevents signature bypass |
| Use short-lived access tokens (15 min) | Limits impact of stolen tokens |
| Implement refresh token rotation | Detects token theft |
Authorization
Server-Side Enforcement
# BAD — relying on UI to hide admin features
@app.get("/admin/users")
async def list_users():
return db.get_all_users()
# GOOD — server-side role check
@app.get("/admin/users")
async def list_users(current_user: User = Depends(get_current_user)):
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Forbidden")
return db.get_all_users()
IDOR (Insecure Direct Object Reference)
# BAD — trusts user-supplied ID
@app.get("/documents/{doc_id}")
async def get_document(doc_id: int):
return db.get_document(doc_id)
# GOOD — scoped to authenticated user
@app.get("/documents/{doc_id}")
async def get_document(
doc_id: int,
current_user: User = Depends(get_current_user),
):
doc = db.get_document(doc_id)
if doc.owner_id != current_user.id:
raise HTTPException(status_code=404, detail="Not found")
return doc
Auth Guard Inversion Check
# WRONG — inverted condition (found in 31% of AI-generated apps)
if session:
return RedirectResponse("/login")
# CORRECT
if not session:
return RedirectResponse("/login")
CORS Configuration
# BAD — allows any origin
app.add_middleware(CORSMiddleware, allow_origins=["*"])
# GOOD — specific origins only
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myapp.com", "https://staging.myapp.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
Never use Access-Control-Allow-Origin: * for authenticated endpoints
Wildcard CORS allows any website to make authenticated requests to your API.
Security Headers
| Header | Value | Purpose |
|---|---|---|
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
Force HTTPS for 1 year |
Content-Security-Policy |
default-src 'self'; script-src 'self' |
Prevent XSS, inline scripts |
X-Content-Type-Options |
nosniff |
Prevent MIME type sniffing |
X-Frame-Options |
DENY |
Prevent clickjacking |
Referrer-Policy |
strict-origin-when-cross-origin |
Control referrer leakage |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
Disable unused browser APIs |
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response: Response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Permissions-Policy"] = "camera=(), microphone=()"
return response
Test headers at securityheaders.com.
Rate Limiting
| Endpoint Type | Limit | Window |
|---|---|---|
| Login / Registration / Password Reset | 5-10 requests | 15 minutes |
| General API (authenticated) | 100 requests | 15 minutes |
| General API (unauthenticated) | 30 requests | 15 minutes |
| File uploads | 10 requests | 1 hour |
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@app.post("/login")
@limiter.limit("5/15minutes")
async def login(request: Request):
...
Error Handling
# BAD — leaks internal details
@app.exception_handler(Exception)
async def error_handler(request, exc):
return JSONResponse(
status_code=500,
content={"error": str(exc), "traceback": traceback.format_exc()},
)
# GOOD — generic error in production
@app.exception_handler(Exception)
async def error_handler(request, exc):
logger.exception("Unhandled error", extra={"path": request.url.path})
return JSONResponse(
status_code=500,
content={"error": "Internal server error"},
)
Never expose in error responses
Stack traces, file paths, database table names, SQL queries, internal IP addresses, library versions.