How to secure a Flask app – Step by Step

Sat Oct 11 2025

This guide is a concise, code-first hardening checklist to lock down Flask.

TL;DR checklist

  • Secrets in env/secret store (never in git)
  • HTTPS everywhere + HSTS (1y+)
  • Security headers (CSP, X-Frame-Options, Referrer-Policy, etc.)
  • Strict cookies and sessions (Secure, HttpOnly, SameSite)
  • CSRF protection on state-changing routes
  • Authentication with strong hashing (argon2/bcrypt) and MFA where possible
  • Rate limiting on auth and sensitive endpoints
  • Input validation and safe file uploads
  • Dependency pinning and security scanning (pip-audit, bandit)
  • Proper reverse proxy + WSGI (gunicorn/uwsgi) and non-root containers
  • Structured audit logs + alerting

1) Configuration and secrets

# app/config.py
import os

class Settings:
    secret_key = os.environ["FLASK_SECRET_KEY"]
    session_cookie_secure = True
    session_cookie_httponly = True
    session_cookie_samesite = "Lax"  # or "Strict" for same-site apps
    remember_cookie_duration = 0

settings = Settings()

Use a secrets manager (AWS Secrets Manager, GCP Secret Manager, 1Password Connect) or at least .env (development only). Never commit secrets.

# .env (dev only)
FLASK_SECRET_KEY='change-me'
DATABASE_URL='postgresql://user:pass@localhost:5432/app'

2) HTTPS + HSTS (app-level)

flask-talisman is a lightweight Flask extension that sets sane HTTP security headers for you and can force HTTPS/HSTS. Why this matters:

  • Prevents protocol downgrade and mixed‑content issues (HSTS)
  • Reduces clickjacking and information leakage (X‑Frame‑Options, Referrer‑Policy)
  • Gives you a configurable CSP baseline to curb XSS

Notes:

  • Keep HTTPS forcing off in local dev or set app.debug = True.

  • Behind a proxy, ensure X-Forwarded-Proto is set so Talisman detects HTTPS.

  • PyPI: Flask-Talisman

  • Install:

pip install flask-talisman
# app/security.py
from flask import Flask
from flask_talisman import Talisman

csp = {
    'default-src': ["'self'"],
    'script-src': ["'self'", "'unsafe-inline'"],  # switch to nonces below for stricter CSP
    'style-src': ["'self'", "'unsafe-inline'"],
    'img-src': ["'self'", 'data:'],
}

def apply_security(app: Flask) -> None:
    Talisman(
        app,
        content_security_policy=csp,
        force_https=True,
        strict_transport_security=True,
        strict_transport_security_max_age=31536000,
        frame_options='SAMEORIGIN',
        referrer_policy='no-referrer',
        session_cookie_secure=True,
        session_cookie_http_only=True,
    )

3) Sessions and cookies

Prefer server-side sessions (e.g., Redis) over client-side signed cookies for sensitive apps.

Why:

  • Server-side sessions let you invalidate sessions instantly (logouts, revocations)
  • You avoid large, tamper‑resistant but still replayable client cookies
  • Easier to store additional auth context securely (roles, anti‑replay tokens)

If you must use cookie sessions, keep cookies short‑lived, set Secure+HttpOnly+SameSite, and rotate keys.

Library: Flask-Session

Install:

pip install Flask-Session
from flask import Flask
from flask_session import Session

def configure_sessions(app: Flask) -> None:
    app.config.update(
        SESSION_TYPE='redis',
        SESSION_COOKIE_SECURE=True,
        SESSION_COOKIE_HTTPONLY=True,
        SESSION_COOKIE_SAMESITE='Lax',
        PERMANENT_SESSION_LIFETIME=0,
    )
    Session(app)

4) CSRF protection

Library: Flask-WTF

Install:

pip install flask-wtf

Why:

  • CSRF exploits the browser’s cookies; tokens bind a user action to a form/page
  • SameSite=Lax helps but is not sufficient for all flows (e.g., some OAuth/POST)
from flask_wtf import CSRFProtect

csrf = CSRFProtect()

def enable_csrf(app):
    app.config['WTF_CSRF_TIME_LIMIT'] = None
    csrf.init_app(app)

5) Authentication, hashing, and rate limiting

Libraries:

Install:

pip install Flask-Limiter
from werkzeug.security import generate_password_hash, check_password_hash
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(key_func=get_remote_address, default_limits=["200/min"])

def hash_password(pw: str) -> str:
    return generate_password_hash(pw, method='scrypt')  # or 'pbkdf2:sha256', 'bcrypt', 'argon2'

def verify_password(hash_: str, pw: str) -> bool:
    return check_password_hash(hash_, pw)

def protect_auth(app):
    limiter.init_app(app)

    @app.post('/login')
    @limiter.limit("5/minute")
    def login():
        ...

Why:

  • Slow down credential‑stuffing and brute‑force attempts (Limiter)
  • Store only password hashes; never plaintext. Prefer memory‑hard KDFs

Optional (Argon2):

pip install argon2-cffi
from argon2 import PasswordHasher
ph = PasswordHasher()
hash_ = ph.hash(password)
ph.verify(hash_, candidate)

6) Input validation & uploads

Use pydantic/marshmallow to reject bad input early. For files: verify MIME by content (magic), enforce size/extension allow‑lists, and randomize filenames.

Libraries:

  • pydantic or marshmallow
    • pydantic: fast validation, type‑driven models
    • marshmallow: explicit schemas/serialization control

Install one:

pip install pydantic
# or
pip install marshmallow
from pydantic import BaseModel, EmailStr, constr

class RegisterBody(BaseModel):
    email: EmailStr
    password: constr(min_length=12)

7) Content Security Policy (nonce pattern)

Why:

  • A strict CSP with nonces blocks inline/script injection and most reflected XSS
  • Nonces work better than hashes when content changes per request
from flask import g
import secrets

@app.before_request
def set_nonce():
    g.csp_nonce = secrets.token_urlsafe(16)

# Jinja: <script nonce="{{ g.csp_nonce }}">...</script>

Update your CSP to include script-src 'self' 'nonce-{value}' and issue a fresh nonce per request; replace any inline event handlers with proper scripts.

8) Dependency hygiene (code-focused)

pip install --upgrade pip pip-tools pip-audit bandit
pip-compile -o requirements.txt pyproject.toml  # or pin with pip-tools
pip-audit
bandit -r app/

9) Observability and logs

Emit JSON logs and forward to your SIEM. Consider Sentry or OpenTelemetry for traces.

import json, logging, sys

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter('%(message)s'))

logger = logging.getLogger('app')
logger.setLevel(logging.INFO)
logger.addHandler(handler)

def audit(event: str, **kwargs):
    logger.info(json.dumps({'event': event, **kwargs}))

10) Safe defaults you should set

app.config.update(
    JSONIFY_PRETTYPRINT_REGULAR=False,
    PROPAGATE_EXCEPTIONS=False,
    MAX_CONTENT_LENGTH=10 * 1024 * 1024,  # 10 MB uploads
    SEND_FILE_MAX_AGE_DEFAULT=31536000,
)

11) Bonus hardening snippets

Add request size limits, disable server error leaks, and sanitize headers.

from flask import Flask, request

def extra_hardening(app: Flask):
    app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024

    @app.after_request
    def remove_server_header(resp):
        resp.headers.pop('Server', None)
        return resp

    @app.before_request
    def block_weird_methods():
        if request.method not in { 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS' }:
            return ("", 405)

Final checklist before go-live

  • TLS + HSTS enabled and verified
  • CSP enforced (report-only first, then enforce)
  • Cookies: Secure+HttpOnly+SameSite, session lifetime minimal
  • CSRF on all unsafe methods
  • Rate limits on auth/sensitive routes
  • Pinned deps, pip-audit clean, bandit reviewed
  • Logs structured and shipped; alerts configured