Skip to main content
Back to blog
April 20, 2026·Updated May 10, 2026|8 min read|Anay Pandya

Flask & FastAPI Security Guide 2026: HTTPS, CORS, Pydantic & Rate Limiting

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.

Anay Pandya

1,871 words

AP

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

Scan free →

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.

bash
pip install flask-talisman
python
from 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:

python
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:

bash
pip install flask-cors
python
from 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:

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

python
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:

python
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 validated

4. Flask Session Security

Flask's default cookie-based session uses a signed but readable cookie. For sensitive applications, use server-side sessions.

bash
pip install Flask-Session redis
python
from 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:

python
from flask import session
session.clear()
session.permanent = True
session['user_id'] = user.id

5. Rate Limiting with Flask-Limiter

bash
pip install Flask-Limiter
python
from 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():
    # ...
    pass

For FastAPI, use slowapi:

python
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):
    # ...
    pass

6. Environment Hardening and Secret Management

python
# 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 set

Use python-decouple for typed environment variables:

python
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:

bash
# 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:

python
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:

python
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:

python
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 response

secure 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:

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

python
# 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

python
# 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

python
# 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

python
# 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"}, 500

CI/CD Integration for Flask and FastAPI Apps

Add security scanning to your GitHub Actions pipeline to catch vulnerabilities before they reach production:

yaml
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 check

bandit 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

Run free scan

Related articles

Keep reading