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
| Secret | Rule |
|---|---|
SECRET_KEY | Random per environment; rotate if leaked |
JWT_SECRET_KEY | Separate from session secret if using JWT |
| Database password | Environment variable only |
| API keys | Never in Git—use .env locally, vault in prod |
Generate:
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:
<form method="post">
{{ form.hidden_tag() }}
...
</form>Config:
WTF_CSRF_ENABLED = TrueJSON APIs using JWT typically skip CSRF—cookies + session auth need CSRF or SameSite strategy.
Secure Cookies
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:
HttpOnlyreduces XSS cookie theftSecuresends cookies only over HTTPSSameSite=Laxmitigates many CSRF cases
XSS and Jinja2
Default auto-escape:
<p>{{ user_bio }}</p>Dangerous:
<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:
db.session.execute(f"SELECT * FROM users WHERE name = '{name}'")Good—ORM or bound parameters:
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):
@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 responseFlask-Talisman (concept):
pip install flask-talismanfrom 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_filenameMAX_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
nextredirect URLs (relative paths only) - Lock accounts after repeated failures (application logic)
Dependency Vulnerabilities
pip auditOr GitHub Dependabot on the repo—see Git CI/CD.
Update Flask and extensions when security advisories publish.
Debug and Error Leaks
DEBUG = False
PROPAGATE_EXCEPTIONS = FalseCustom 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_KEYfrom environment -
DEBUG=Falsein production - CSRF on state-changing HTML forms
- Secure cookie flags with HTTPS
- No
|safeon user HTML without sanitization - ORM/parameterized SQL only
- Upload validation and size limits
- Rate limits on auth endpoints
-
pip auditin 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.