Anay Pandya
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
- Flask security requires assembling the right extensions — the micro-framework ships with minimal defaults. This guide covers Flask-Talisman, Flask-CORS, and FastAPI's built-in security utilities.
- Includes copy-paste code examples and step-by-step instructions.
- Free automated scan available to verify your implementation.
Flask & FastAPI Security Guide 2026: HTTPS, CORS, Pydantic & Rate Limiting
Flask security starts with a blank slate — Flask's philosophy is to stay out of your way, which means security is your responsibility from the first line of code. FastAPI is more opinionated about validation, but its security configuration still requires deliberate setup. This guide covers both frameworks comprehensively.
<div class="zf-stat-callout" style="background:#0d1117;border:1px solid rgba(16,185,129,0.25);border-left:3px solid #10b981;border-radius:4px;padding:16px 20px;margin:24px 0"> <p style="margin:0 0 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.15em;color:#10b981;font-family:monospace">ZeriFlow Data — 12,400+ sites analyzed</p> <p style="margin:0;font-size:13px;color:#e2e8f0;line-height:1.6;font-family:monospace">ZeriFlow scan data: only 41% of analyzed sites implement HTTP Strict Transport Security (HSTS). Without it, users on captive portals or hotel Wi-Fi are trivially redirected to HTTP.</p> </div>
Is your site actually secure?
Run a free check — 60 seconds
Before starting, run a free scan at ZeriFlow to get an instant view of your current live security posture across 80+ checks.
1. Flask-Talisman: Security Headers in One Extension
Flask-Talisman wraps Flask's response pipeline and sets HTTP security headers including Content Security Policy and HTTPS enforcement.
pip install flask-talismanfrom flask import Flask
from flask_talisman import Talisman
app = Flask(__name__)
csp = {
'default-src': "'self'",
'script-src': "'self'",
'style-src': ["'self'", 'https://fonts.googleapis.com'],
'font-src': ["'self'", 'https://fonts.gstatic.com'],
'img-src': ["'self'", 'data:', 'https:'],
}
Talisman(
app,
force_https=True,
strict_transport_security=True,
strict_transport_security_max_age=31536000,
strict_transport_security_include_subdomains=True,
strict_transport_security_preload=True,
content_security_policy=csp,
frame_options='DENY',
referrer_policy='strict-origin-when-cross-origin',
feature_policy={
'geolocation': "'none'",
'microphone': "'none'",
'camera': "'none'",
},
)For FastAPI, use starlette-csrf and a custom middleware:
from fastapi import FastAPI
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
app = FastAPI()
app.add_middleware(HTTPSRedirectMiddleware)
app.add_middleware(TrustedHostMiddleware, allowed_hosts=['yourdomain.com', '*.yourdomain.com'])2. CORS Configuration
Flask-CORS:
pip install flask-corsfrom flask_cors import CORS
CORS(
app,
origins=['https://yourdomain.com', 'https://app.yourdomain.com'],
methods=['GET', 'POST', 'PUT', 'DELETE'],
allow_headers=['Content-Type', 'Authorization'],
supports_credentials=True,
max_age=86400,
)FastAPI built-in CORS:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=['https://yourdomain.com'],
allow_credentials=True,
allow_methods=['*'],
allow_headers=['Authorization', 'Content-Type'],
max_age=86400,
)Never set allow_origins=['*'] on an authenticated API. Wildcard origins bypass the same-origin policy and allow any website to read your API responses in the user's browser.
3. Pydantic Validation in FastAPI
FastAPI uses Pydantic models for request validation. This is one of FastAPI's strongest security features — all input is validated and typed before your handler runs.
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, EmailStr, field_validator
import re
class UserCreate(BaseModel):
name: str
email: EmailStr
password: str
@field_validator('name')
@classmethod
def name_must_be_clean(cls, v: str) -> str:
if len(v) < 2 or len(v) > 100:
raise ValueError('Name must be 2-100 characters')
if not re.match(r'^[a-zA-Z\s\-]+$', v):
raise ValueError('Name contains invalid characters')
return v.strip()
@field_validator('password')
@classmethod
def password_strength(cls, v: str) -> str:
if len(v) < 12:
raise ValueError('Password must be at least 12 characters')
if not re.search(r'[A-Z]', v):
raise ValueError('Password must contain uppercase letter')
if not re.search(r'[0-9]', v):
raise ValueError('Password must contain a digit')
return v
@app.post('/users', status_code=status.HTTP_201_CREATED)
async def create_user(payload: UserCreate):
# payload is fully validated at this point
hashed = hash_password(payload.password)
# ...For Flask, use marshmallow or pydantic directly:
from pydantic import BaseModel, EmailStr, ValidationError
class UserSchema(BaseModel):
name: str
email: EmailStr
@app.route('/users', methods=['POST'])
def create_user():
try:
data = UserSchema(**request.get_json())
except ValidationError as e:
return {'error': e.errors()}, 400
# data.name and data.email are validated4. Flask Session Security
Flask's default cookie-based session uses a signed but readable cookie. For sensitive applications, use server-side sessions.
pip install Flask-Session redisfrom flask import Flask
from flask_session import Session
import redis
app = Flask(__name__)
app.config.update(
SECRET_KEY = os.environ['SECRET_KEY'],
SESSION_TYPE = 'redis',
SESSION_REDIS = redis.from_url(os.environ['REDIS_URL']),
SESSION_COOKIE_SECURE = True,
SESSION_COOKIE_HTTPONLY = True,
SESSION_COOKIE_SAMESITE = 'Lax',
PERMANENT_SESSION_LIFETIME = 1800, # 30 minutes
)
Session(app)Always regenerate the session on login:
from flask import session
session.clear()
session.permanent = True
session['user_id'] = user.id5. Rate Limiting with Flask-Limiter
pip install Flask-Limiterfrom flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=['200 per day', '50 per hour'],
storage_uri=os.environ['REDIS_URL'],
)
@app.route('/auth/login', methods=['POST'])
@limiter.limit('10 per 15 minutes')
def login():
# ...
passFor FastAPI, use slowapi:
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.post('/auth/login')
@limiter.limit('10/15minutes')
async def login(request: Request, credentials: LoginSchema):
# ...
pass6. Environment Hardening and Secret Management
# Never hardcode secrets
# Bad:
app.secret_key = 'mysecretkey'
# Good:
import os
app.secret_key = os.environ['SECRET_KEY']
# Use python-dotenv for local development only
from dotenv import load_dotenv
load_dotenv() # Loads .env in development, ignored in production when env vars are setUse python-decouple for typed environment variables:
from decouple import config, Csv
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv())Run ZeriFlow after every production deployment to confirm .env files and debug endpoints are not publicly accessible.
FAQ
### Q: Is Flask or FastAPI more secure by default? A: FastAPI provides better security defaults through mandatory Pydantic validation and async-first architecture. Flask is more flexible but requires more manual security setup. Both can be made equally secure with the right extensions.
### Q: How do I prevent SQL injection in Flask with SQLAlchemy?
A: Use SQLAlchemy's ORM or its parameterized text() construct. Never format user input into SQL strings. The ORM handles parameterization automatically for all standard queries.
### Q: Should I use Flask-Login for authentication?
A: Flask-Login handles the session management layer (current_user, login_required) but not password hashing or form validation. Combine it with bcrypt or argon2-cffi for passwords and WTForms or Pydantic for validation.
### Q: How do I handle file uploads securely in Flask?
A: Use werkzeug.utils.secure_filename(), validate MIME type (not just extension), restrict allowed types, set a max content length via MAX_CONTENT_LENGTH, and store files outside the webroot.
### Q: Does FastAPI automatically handle CSRF?
A: No. FastAPI is primarily an API framework — CSRF is relevant when browser cookies are used for authentication. If you use cookie-based sessions, add starlette-csrf middleware. If you use Bearer tokens in headers, CSRF is not applicable.
Conclusion
Flask and FastAPI security in 2026 means assembling the right pieces: Talisman or custom middleware for headers, strict CORS, Pydantic or marshmallow for validation, Redis-backed rate limiting, and server-side sessions for Flask. FastAPI's Pydantic integration is a genuine security advantage — leverage it on every endpoint.
Validate your configuration from the outside with ZeriFlow's free scanner — it checks your headers, TLS grade, and exposed endpoints in seconds, giving you the same view an attacker would have before launching an attack.
Further Reading
<!-- zf-internal-links -->
Testing Your Flask Security Headers
After implementing security headers, validate them automatically:
# Quick check from the command line
curl -I https://your-app.com | grep -E "(Strict|Content-Security|X-Frame|X-Content|Referrer)"Or use Python to test programmatically:
import requests
def test_security_headers(url):
r = requests.get(url)
required = {
"Strict-Transport-Security": "max-age=",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Content-Security-Policy": "default-src",
"Referrer-Policy": "strict-origin",
}
for header, expected in required.items():
value = r.headers.get(header, "MISSING")
status = "PASS" if expected in value else "FAIL"
print(f"{status} {header}: {value}")
test_security_headers("https://your-app.com")Run this script in your test suite against a staging environment before every production deploy. ZeriFlow's scanner extends this to 80+ checks including TLS cipher strength, HSTS preload eligibility, and subdomain configuration.
FastAPI Security Middleware
FastAPI doesn't include security headers by default. Use starlette middleware to add the missing layer:
from fastapi import FastAPI
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
app = FastAPI()
# Force HTTPS in production
app.add_middleware(HTTPSRedirectMiddleware)
# Only accept requests from your domain
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["your-app.com", "www.your-app.com"]
)For a complete security header suite, use the secure library which provides a framework-agnostic header builder:
from secure import Secure
secure_headers = Secure()
@app.middleware("http")
async def set_secure_headers(request, call_next):
response = await call_next(request)
secure_headers.framework.fastapi(response)
return responsesecure sets X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security, Referrer-Policy, and Permissions-Policy in a single call with sensible defaults. Override individual headers by instantiating Secure with explicit values:
from secure import Secure, ContentSecurityPolicy, StrictTransportSecurity
csp = ContentSecurityPolicy().default_src("'self'").script_src("'self'", "https://cdn.jsdelivr.net")
hsts = StrictTransportSecurity().max_age(63072000).include_subdomains().preload()
secure_headers = Secure(csp=csp, hsts=hsts)Common Flask Security Mistakes
1. Debug mode in production
Debug mode exposes an interactive Python shell via the Werkzeug debugger — any visitor who triggers an exception can execute arbitrary code on your server.
# Never do this in production
app.run(debug=True)
# Use environment variables to control debug state
app.run(debug=os.getenv("FLASK_DEBUG", "false").lower() == "true")2. Insecure session configuration
# Weak session cookie settings — transmits over HTTP, readable by JavaScript
app.config["SESSION_COOKIE_SECURE"] = False
app.config["SESSION_COOKIE_HTTPONLY"] = False
# Hardened session settings
app.config.update(
SESSION_COOKIE_SECURE=True, # HTTPS only
SESSION_COOKIE_HTTPONLY=True, # No JavaScript access
SESSION_COOKIE_SAMESITE="Strict", # Block cross-site requests
PERMANENT_SESSION_LIFETIME=timedelta(hours=1),
)3. SQL injection via string formatting
# Vulnerable to SQL injection — user_input ends the query and injects SQL
query = f"SELECT * FROM users WHERE email = '{user_input}'"
db.execute(query)
# Parameterized query — database driver handles escaping
db.execute("SELECT * FROM users WHERE email = ?", (user_input,))
# With SQLAlchemy ORM — parameterized automatically
user = db.session.query(User).filter_by(email=user_input).first()4. Returning stack traces to clients
# Default Flask behavior in development — leaks file paths and code
@app.errorhandler(500)
def server_error(e):
return str(e), 500 # Never in production
# Return a generic error in production
@app.errorhandler(500)
def server_error(e):
app.logger.error(f"Internal error: {e}")
return {"error": "Internal server error"}, 500CI/CD Integration for Flask and FastAPI Apps
Add security scanning to your GitHub Actions pipeline to catch vulnerabilities before they reach production:
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -r requirements.txt bandit pip-audit
- name: Run Bandit static analysis
run: bandit -r app/ -ll --exit-zero
- name: Audit dependencies for known CVEs
run: pip-audit --requirement requirements.txt
- name: Run safety check
run: pip install safety && safety checkbandit performs static analysis for 40+ Python security anti-patterns including hardcoded secrets, use of subprocess with shell=True, weak cryptography, and insecure deserialization. pip-audit cross-references your dependency tree against the Python Advisory Database (PyPA).
ZeriFlow's CI/CD integration extends this pipeline to scan your deployed endpoint after every merge — confirming that configuration changes made at the infrastructure level (nginx, load balancer, CDN) did not remove headers your app was relying on.
Scan your API's security headers and TLS configuration.
80+ automated checks in 60 seconds — free.
Related resources
Keep improving your website security
Related tools
Website Vulnerability Scanner
Run a broader website security audit across headers, TLS, DNS, cookies, SEO, and disclosure checks.
Security Headers Checker
Check CSP, HSTS, X-Frame-Options, and other response headers.
SSL Checker
Review TLS certificate, HTTPS, and transport security signals.
DMARC Checker
Validate email authentication records for domain spoofing protection.
CSP Checker
Review Content-Security-Policy coverage and common gaps.