Flask's greatest strength is also its biggest security caveat: it does almost nothing you did not ask for. There is no built-in ORM guarding you from SQL injection, no automatic CSRF protection, and no security-header middleware. That minimalism is why Flask is a joy to work with and why insecure Flask apps are so common. The framework will happily let you do the wrong thing.
Where Flask's defaults leave gaps
Compared to a batteries-included framework, a bare Flask app ships without CSRF protection, without a Content-Security-Policy, without secure cookie flags, and with a debug mode that exposes an interactive Python console to anyone who can trigger an exception. Every one of those is a control you must add. The good news is that the additions are small and well supported.
Never run the debugger in production
Flask's debug mode enables the Werkzeug interactive debugger, which is effectively remote code execution for anyone who reaches a traceback page. This is not hardening advice; it is a hard rule.
# NEVER do this on a public host
app.run(debug=True)
# Production is served by a WSGI server, not app.run()
# gunicorn -w 4 -b 0.0.0.0:8000 app:app
Gate debug on an environment variable and default it to off, so a stray commit cannot flip it on in production.
Configure the session cookie properly
Flask signs its session cookie but does not encrypt it, and the signature is only as strong as your SECRET_KEY. Load the key from the environment, make it long and random, and set the cookie flags explicitly:
import os
app.config.update(
SECRET_KEY=os.environ["FLASK_SECRET_KEY"],
SESSION_COOKIE_SECURE=True, # only sent over HTTPS
SESSION_COOKIE_HTTPONLY=True, # not readable by JavaScript
SESSION_COOKIE_SAMESITE="Lax", # CSRF defense-in-depth
PERMANENT_SESSION_LIFETIME=1800,
)
Because the session is client-side by default, never store anything sensitive in it beyond a user id. If you need server-side sessions, use Flask-Session backed by Redis.
Add CSRF protection
Flask has no CSRF protection out of the box. Use Flask-WTF, which wires a token into every form:
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
For pure JSON APIs authenticated with bearer tokens rather than cookies, CSRF is not the relevant threat, so use token auth and exempt those routes deliberately rather than disabling protection globally.
Watch for template injection
Flask uses Jinja2, which auto-escapes HTML in .html templates. The dangerous pattern is building a template string from user input, which turns XSS into full server-side template injection (SSTI) and often remote code execution:
from flask import render_template, render_template_string
# DANGEROUS - user input becomes template source (SSTI -> RCE)
render_template_string("Hello " + request.args.get("name"))
# SAFE - user input is passed as data, never as template source
render_template("hello.html", name=request.args.get("name"))
Treat render_template_string with any user-controlled substring as a red flag in code review.
Parameterize every query
Because Flask does not impose an ORM, developers frequently reach for a raw database driver, and that is where injection creeps in:
# DANGEROUS
cur.execute(f"SELECT * FROM users WHERE email = '{email}'")
# SAFE - the driver escapes the parameter
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
Whether you use SQLAlchemy, psycopg, or sqlite3, always pass user data as parameters, never through string formatting.
Set security headers
Add security headers to every response. Flask-Talisman handles HSTS, CSP, and cookie flags in one place:
from flask_talisman import Talisman
Talisman(
app,
content_security_policy={"default-src": "'self'"},
force_https=True,
)
Whether the headers survive your reverse proxy and CDN is best confirmed from the outside with a dynamic scan of the deployed app, which sees exactly what a browser receives rather than what your code intended.
Guard redirects and file uploads
Two smaller footguns catch Flask apps often. The first is the open redirect: passing a user-supplied next parameter straight to redirect() lets an attacker bounce your users to a phishing page that looks like it came from you. Validate that the target is local before you honor it:
from urllib.parse import urlparse
def is_safe_redirect(target: str) -> bool:
parsed = urlparse(target)
# Reject absolute URLs to other hosts; allow only local paths
return not parsed.netloc and not parsed.scheme
The second is file uploads. Never trust the client-supplied filename; run it through werkzeug.utils.secure_filename, validate the content type against an allowlist, cap the size with MAX_CONTENT_LENGTH, and store uploads outside the web root so they cannot be requested back and executed.
Do not leak secrets in config
Flask apps are notorious for a config.py full of hard-coded API keys committed to git. Keep secrets in environment variables or a secrets manager, and scan history for anything that already leaked. Our broader Python secrets management guide covers rotation and detection in depth.
Flask hardening checklist
- Debug mode off in production; served by gunicorn or uwsgi, not
app.run() SECRET_KEYloaded from the environment, long and random- Secure, HttpOnly, SameSite session cookies
- CSRF protection via Flask-WTF on all cookie-authenticated forms
- No
render_template_stringon user input - Parameterized database queries everywhere
- Security headers via Flask-Talisman
- Dependencies pinned and continuously audited
How Safeguard helps
The controls above are code you own, and no tool can write them for you. Where Safeguard fits is the layer beneath your code: the extensions and transitive packages a minimal framework accumulates. Our software composition analysis tracks every dependency from Flask and Werkzeug down to the smallest transitive utility, alerting you when a CVE lands. For the harder judgment calls, Griffin AI explains whether a flagged vulnerability is actually reachable from your routes, so you spend remediation effort where it changes your risk rather than chasing noise. Because Flask projects tend to be small and numerous, that per-service clarity is what keeps a fleet of microservices from hiding a critical CVE.
Get started
Add the framework-level controls first, then let Safeguard watch the dependency layer. Sign up free at app.safeguard.sh/register and read the Flask integration guide at docs.safeguard.sh.