← Back to blog
2026-07-22 · VulnLedger

"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.

Python Flask FastAPI Django Web Security OWASP CSRF XSS

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

VulnerabilityFlask RiskFastAPI RiskDjango Risk
Broken Access ControlHighHighMedium
Cryptographic FailuresMediumMediumLow
Injection (SQL/XSS)HighHighLow
Insecure DesignMediumMediumMedium
Security MisconfigurationHighMediumLow
Vulnerable ComponentsHighHighHigh

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

FeatureFlaskFastAPIDjango
CSRFManual (flask-wtf)ManualBuilt-in
XSSAuto (Jinja2)Auto (Jinja2)Auto
SQL InjectionManualManualBuilt-in
AuthManualManualBuilt-in
Rate LimitingManualManualManual
Security HeadersManualManualBuilt-in
HSTSManualManualBuilt-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

Try VulnLedger

Generate SBOMs and scan for vulnerabilities in one command.

Start Free