How do I secure a Flask app?

Direct answer

Flask ships with almost nothing, so everything is on you. Debug mode off in production, a strong `SECRET_KEY` from the environment, Flask-WTF for CSRF because there's no built-in protection, and ownership checks on every query. The Werkzeug debugger is remote code execution if it reaches production.

Muhammad HasanUpdated

Django gives you secure defaults. Flask gives you a router, and everything else is a decision you have to make on purpose.

Debug mode

app.run(debug=True)   # never in production

The Werkzeug debugger renders an interactive Python console on the error page. Anyone who triggers an exception gets code execution as your application user. It's PIN-protected in recent versions, and the PIN is derived from values that are frequently obtainable.

Serve through gunicorn or uwsgi in production and never pass debug=True. Confirm FLASK_DEBUG isn't set in the deployed environment.

The secret key

app.config['SECRET_KEY'] = os.environ['SECRET_KEY']

Flask sessions are signed with this key and stored in a cookie. A known key means forgeable sessions, including an admin one.

Two specifics worth knowing. A hardcoded development key that ships is the common failure. And Flask's session cookie is signed, not encrypted. Anyone holding it can read the contents, so nothing sensitive goes in the session.

CSRF

There is none by default. Add Flask-WTF:

from flask_wtf.csrf import CSRFProtect
CSRFProtect(app)

If parts of your app are a token-authenticated API, exempt those routes rather than disabling protection globally.

SQL

SQLAlchemy's query interface parameterises. text() with an f-string doesn't:

# Vulnerable
db.session.execute(text(f"SELECT * FROM users WHERE email = '{email}'"))

# Safe
db.session.execute(text("SELECT * FROM users WHERE email = :email"), {"email": email})

Templates

Jinja2 autoescaping is on for .html, .htm, .xml and .xhtml, and off for other extensions. A template named email.txt rendering user content doesn't escape.

|safe and Markup() disable it explicitly. Treat both as review triggers.

Never build a template from user input. render_template_string with user content is server-side template injection, which is code execution rather than XSS.

Ownership

# Vulnerable
order = Order.query.get(order_id)

# Fixed
order = Order.query.filter_by(id=order_id, user_id=current_user.id).first_or_404()

@login_required confirms someone is logged in and nothing more. This was the highest-impact pattern in our 24-app study, and Flask's minimalism makes it easier than usual to overlook.

The rest

Flask-Talisman for security headers and HTTPS enforcement. Flask-Limiter for rate limiting, particularly on login. Set SESSION_COOKIE_SECURE, SESSION_COOKIE_HTTPONLY and SESSION_COOKIE_SAMESITE. Validate uploads and store them outside the static directory.

bandit is a reasonable free Python analyser and catches the raw SQL and template injection cases.

Go deeper

Kolega for AppSec teams

Related answers

See what your own repository returns

Connect a repo and run a scan. No credit card, no pipeline changes.

Get started for free