"Python Web App Security: Flask, FastAPI, Django Hardening Guide"
Security hardening guide for Flask, FastAPI, and Django applications. Covers CSRF, XSS, SQL injection, authentication, rate limiting, and security headers. Framework-specific examples.
Your Python web app has a login page, API endpoints, and a database. It probably works. But is it secure? Most Python web apps ship with at least 3 OWASP Top 10 vulnerabilities.
This guide hardens Flask, FastAPI, and Django applications against the most common attacks. Framework-specific, practical, no theory.
OWASP Top 10 in Python Web Apps
| Vulnerability | Flask Risk | FastAPI Risk | Django Risk |
|---|---|---|---|
| Broken Access Control | High | High | Medium |
| Cryptographic Failures | Medium | Medium | Low |
| Injection (SQL/XSS) | High | High | Low |
| Insecure Design | Medium | Medium | Medium |
| Security Misconfiguration | High | Medium | Low |
| Vulnerable Components | High | High | High |
Django has built-in protections for most OWASP categories. Flask and FastAPI require manual hardening.
Flask Security Hardening
CSRF Protection
from flask import Flask
from flask_wtf.csrf import CSRFProtect
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
csrf = CSRFProtect(app)
# Every form must include: {{ csrf_token() }}
SQL Injection Prevention
# WRONG
db.execute(f"SELECT * FROM users WHERE id = {user_id}")
# RIGHT — SQLAlchemy ORM
user = db.query(User).filter(User.id == user_id).first()
# RIGHT — raw query
db.execute("SELECT * FROM users WHERE id = :id", {"id": user_id})
XSS Prevention
# Jinja2 auto-escapes by default — good
return render_template("profile.html", name=user_input)
# But f-strings don't escape — bad
return f"<div>{user_input}</div>"
# Always use render_template for HTML output
Security Headers
@app.after_request
def add_headers(response):
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Strict-Transport-Security'] = 'max-age=31536000'
response.headers['Content-Security-Policy'] = "default-src 'self'"
return response
FastAPI Security Hardening
CORS Configuration
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourdomain.com"], # Never use "*" in production
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["*"],
)
Authentication
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer
security = HTTPBearer()
async def get_current_user(token: str = Depends(security)):
try:
payload = decode_token(token.credentials)
return payload
except:
raise HTTPException(status_code=401, detail="Invalid token")
@app.get("/protected")
async def protected(user = Depends(get_current_user)):
return {"message": f"Hello {user['sub']}"}
Rate Limiting
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
@app.get("/api/data")
@limiter.limit("10/minute")
async def get_data():
return {"data": "value"}
Django Security (Built-in)
Django has the best built-in security of any Python framework:
# settings.py — already configured by default
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = 'DENY'
Django's ORM prevents SQL injection by default. Template engine escapes HTML by default. CSRF is built-in. You still need to:
- Keep Django updated
- Use select_related/prefetch_related to prevent N+1 (not security, but performance)
- Add rate limiting (not built-in)
Universal Security Practices
Password Hashing
# WRONG
import hashlib
hashed = hashlib.sha256(password.encode()).hexdigest()
# RIGHT
from passlib.hash import bcrypt
hashed = bcrypt.hash(password)
# Verify
bcrypt.verify(password, hashed)
JWT Best Practices
import jwt
from datetime import datetime, timedelta
# Generate
token = jwt.encode({
"sub": user_id,
"exp": datetime.utcnow() + timedelta(hours=1),
"iat": datetime.utcnow(),
}, os.environ['SECRET_KEY'], algorithm="HS256")
# Verify
try:
payload = jwt.decode(token, os.environ['SECRET_KEY'], algorithms=["HS256"])
except jwt.ExpiredSignatureError:
# Token expired
pass
except jwt.InvalidTokenError:
# Invalid token
pass
Environment Variables
# NEVER hardcode
SECRET_KEY = "super-secret" # BAD
# ALWAYS use environment
SECRET_KEY = os.environ.get("SECRET_KEY")
if not SECRET_KEY:
raise ValueError("SECRET_KEY not set")
Framework Comparison
| Feature | Flask | FastAPI | Django |
|---|---|---|---|
| CSRF | Manual (flask-wtf) | Manual | Built-in |
| XSS | Auto (Jinja2) | Auto (Jinja2) | Auto |
| SQL Injection | Manual | Manual | Built-in |
| Auth | Manual | Manual | Built-in |
| Rate Limiting | Manual | Manual | Manual |
| Security Headers | Manual | Manual | Built-in |
| HSTS | Manual | Manual | Built-in |
Recommendation: Django for maximum security with minimal effort. Flask/FastAPI for flexibility with more manual hardening.
Security Testing
# Dependency scanning
pip install vulnledger
vulnledger scan . --ci
# Secret detection
pip install detect-secrets
detect-secrets scan
# Bandit (Python security linter)
pip install bandit
bandit -r . --severity-level medium
Conclusion
Python web app security depends on your framework. Django has the most built-in protections. Flask and FastAPI require more manual hardening. But all three need dependency scanning, secrets management, and CI/CD security gates.
Previous: Securing Your Python Dependencies
Next: Python Container Security: Docker + Python
See also: Input Validation: The #1 Thing AI-Generated Code Gets Wrong