Flask Security Best Practices

Introduction

Security mistakes in web apps lead to stolen sessions, data leaks, and defaced sites. Flask gives safe defaults in some areas (Jinja2 escaping) but you must configure secrets, CSRF, cookies, headers, and database access correctly. This chapter collects the essential checklist for Flask apps before production deploy.

Prerequisites

Secrets and Environment

SecretRule
SECRET_KEYRandom per environment; rotate if leaked
JWT_SECRET_KEYSeparate from session secret if using JWT
Database passwordEnvironment variable only
API keysNever in Git—use .env locally, vault in prod

Generate:

bash
python -c "import secrets; print(secrets.token_hex(32))"

Warning

No Secrets in Git History

Removing a committed key requires rotation—.gitignore alone does not fix past commits.

CSRF Protection

All browser POST forms that change state need CSRF tokens:

html
<form method="post">
  {{ form.hidden_tag() }}
  ...
</form>

Config:

python
WTF_CSRF_ENABLED = True

JSON APIs using JWT typically skip CSRF—cookies + session auth need CSRF or SameSite strategy.

Secure Cookies

python
app.config.update(
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SECURE=True,       # HTTPS only
    SESSION_COOKIE_SAMESITE="Lax",
    REMEMBER_COOKIE_HTTPONLY=True,
    REMEMBER_COOKIE_SECURE=True,
)

Code explanation:

  • HttpOnly reduces XSS cookie theft
  • Secure sends cookies only over HTTPS
  • SameSite=Lax mitigates many CSRF cases

XSS and Jinja2

Default auto-escape:

html
<p>{{ user_bio }}</p>

Dangerous:

html
<p>{{ user_bio|safe }}</p>

Use |safe only for trusted admin-generated HTML. Prefer Markdown rendered through a sanitizer library.

SQL Injection

Bad—never concatenate user input:

python
db.session.execute(f"SELECT * FROM users WHERE name = '{name}'")

Good—ORM or bound parameters:

python
User.query.filter_by(username=name).first()
 
from sqlalchemy import text
db.session.execute(text("SELECT * FROM users WHERE name = :n"), {"n": name})

Flask-SQLAlchemy ORM paths are safe when you do not inject raw SQL strings.

Security Response Headers

Manual (in after_request):

python
@app.after_request
def security_headers(response):
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "SAMEORIGIN"
    response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
    return response

Flask-Talisman (concept):

bash
pip install flask-talisman
python
from flask_talisman import Talisman
Talisman(app, content_security_policy={"default-src": "'self'"})

Content-Security-Policy limits script sources—tune for CDNs and inline scripts carefully.

File Upload Safety

From file uploads chapter:

  • secure_filename
  • MAX_CONTENT_LENGTH
  • Validate type beyond extension
  • Store outside web root; serve through authenticated views

Authentication Hardening

  • Hash passwords with werkzeug.security—see Flask-Login chapter
  • Rate-limit login—Flask-Limiter on /login
  • Validate next redirect URLs (relative paths only)
  • Lock accounts after repeated failures (application logic)

Dependency Vulnerabilities

bash
pip audit

Or GitHub Dependabot on the repo—see Git CI/CD.

Update Flask and extensions when security advisories publish.

Debug and Error Leaks

python
DEBUG = False
PROPAGATE_EXCEPTIONS = False

Custom 500 page without stack trace in production—see error handling.

HTTPS Everywhere

Terminate TLS at Nginx or load balancer—see deployment chapter. Never send session cookies over plain HTTP in production.

Security Checklist

  • Strong SECRET_KEY from environment
  • DEBUG=False in production
  • CSRF on state-changing HTML forms
  • Secure cookie flags with HTTPS
  • No |safe on user HTML without sanitization
  • ORM/parameterized SQL only
  • Upload validation and size limits
  • Rate limits on auth endpoints
  • pip audit in CI
  • Security headers or Talisman

FAQ

CSRF on API with Bearer token?

Usually not required—CSRF targets cookie-authenticated browser forms.

SameSite=None?

Needed for cross-site cookies with Secure—prefer same-site SPA + JWT when possible.

SQLAlchemy raw SQL safe?

Only with text() and bound parameters—never f-strings with user input.

Flask secure by default?

Reasonably for templates; misconfiguration (debug, weak secret) causes most incidents.

CSP breaks inline scripts?

Use nonces or move scripts to external files—adjust policy gradually.

pentest before launch?

Recommended for apps handling payments or PII.