Join our Newsletter — 33% off our NHI Course
Home› Guides› Flask Security Guide: Sessions, Secrets and Safe Extensions
Guide Application Security

Flask Security Guide: Sessions, Secrets and Safe Extensions

PDF for newsletter subscribers · free to read online

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 26 October 2026 18 min read
On this page

Flask gives you a small, unopinionated core, which means most of the security decisions that larger frameworks make for you are left to your configuration and your choice of extensions. The default session is a signed cookie the user can read, CSRF protection is not built in, the session cookie is not marked Secure by default, and the interactive debugger will run arbitrary Python for anyone who reaches it. This guide covers the Flask settings, Werkzeug middleware and common extensions (Flask-WTF, Flask-Login) that decide whether a Flask application is safe in production. It is written for Python developers, application security engineers and reviewers who own or assess Flask services, and it goes deeper than the general language guidance in the Python Security Guide.

Key takeaways

  • The SECRET_KEY is the application's master credential. Anyone who holds it can forge session cookies, so generate it with secrets.token_hex(), keep it out of source control, and rotate it with SECRET_KEY_FALLBACKS (Flask 3.1 and later).
  • The default session is signed, not encrypted. Users can read everything you store in session; never put secrets, tokens or authorisation data there that the user should not see.
  • Several cookie defaults are permissive. SESSION_COOKIE_SECURE defaults to False and SESSION_COOKIE_SAMESITE to None; set Secure, HttpOnly and SameSite=Lax explicitly, and do the same for Flask-Login's remember cookie.
  • CSRF protection is your job. Flask has no form framework, so register Flask-WTF's CSRFProtect globally rather than relying on individual forms.
  • Debug mode in production is remote code execution. The Werkzeug debugger executes Python from the browser and its PIN is not a security control; production must use a real WSGI server with debug off.

How Flask's security model differs from batteries-included frameworks

Django and Rails ship CSRF middleware, server-side sessions and security header defaults. Flask deliberately does not. Its own security documentation says it tries to solve a few problems by default and leaves the rest to the application. Knowing which side of that line each control sits on is the first step in any Flask review.

ConcernWhat Flask does by defaultWhat you must add
Cross-site scriptingJinja autoescaping for templates ending in .html, .htm, .xml, .xhtml and .svg, and for render_template_stringQuote attributes, avoid Markup() and |safe on user data, add a Content Security Policy for javascript: URLs
CSRFNothingFlask-WTF CSRFProtect plus SameSite cookies
Session storageClient-side cookie signed with itsdangerous (HMAC, salt cookie-session)Server-side session store if data must be confidential or revocable
Session cookie flagsHttpOnly on; Secure off; SameSite unset; lifetime 31 days when permanentSecure, SameSite=Lax, a shorter PERMANENT_SESSION_LIFETIME
Security headersNoneHSTS, CSP, X-Content-Type-Options, X-Frame-Options (manually or via an extension such as Flask-Talisman)
Request sizeMAX_FORM_MEMORY_SIZE 500 kB and MAX_FORM_PARTS 1,000 (Flask 3.1); MAX_CONTENT_LENGTH unsetSet MAX_CONTENT_LENGTH globally and per view
Host headerAny host acceptedTRUSTED_HOSTS (Flask 3.1)
AuthenticationNothingFlask-Login or an OIDC library, plus password hashing

The practical consequence is that two Flask applications built the same week can have completely different security postures depending on their create_app() factory. Reviews should start by reading the configuration object and the list of registered extensions, not the views.

SECRET_KEY: generation, storage and rotation

Flask uses SECRET_KEY to sign the session cookie, and extensions reuse it: Flask-WTF signs CSRF tokens with it unless WTF_CSRF_SECRET_KEY is set, and Flask-Login's remember cookie is protected by it. A leaked key lets an attacker mint a session cookie for any user ID, which in most applications means full account takeover, including administrator accounts. Treat it as a non-human credential with the same care as a database password.

  • Generation: the Flask documentation recommends a long random value such as the output of python -c 'import secrets; print(secrets.token_hex())'. Never use a word, the project name, or a value copied from a tutorial; values from public examples are trivially guessable and are among the first things an attacker tries.
  • Storage: load it at start-up from a secrets manager or an injected environment variable. Flask's app.config.from_prefixed_env() reads variables prefixed FLASK_, so FLASK_SECRET_KEY becomes SECRET_KEY. Keep it out of config.py, .env files committed to Git, container images and Python packages. See the Secrets Management Guide.
  • Separation: use a different key per environment. A staging key reused in production means anyone with staging access can forge production sessions.
  • Rotation: since Flask 3.1, SECRET_KEY_FALLBACKS holds old keys that are still accepted for unsigning. Put the new key in SECRET_KEY, move the old one to the fallbacks list, and remove it after the longest session lifetime has passed. Flask tests the last fallback first, and extensions that use SECRET_KEY may not support fallbacks yet, so test CSRF and remember-me after a rotation.
  • Emergency rotation: if the key has leaked, do not use fallbacks. Replace the key outright so every existing session and remember cookie becomes invalid, and accept that all users must log in again.

Hard-coded keys reach the public in the same ways as any other secret. The PyPI Secrets Exposure 2023 research found thousands of credentials in configuration files, test folders and READMEs shipped inside published Python packages, because packaging does not honour .gitignore by default. A Flask project that ships its instance/config.py inside a wheel has published its session signing key.

Sessions and cookies

Flask's default SecureCookieSessionInterface serialises the session dictionary, signs it with URLSafeTimedSerializer and sends it to the browser. The Flask quickstart is explicit that the user can see the contents but cannot modify them without the key. That design has three consequences: data in the session is not confidential, a session cannot be revoked on the server because the server keeps no record of it, and a stolen cookie stays valid until its signature ages past PERMANENT_SESSION_LIFETIME.

Recommended cookie configuration

SettingDefaultRecommendedWhy
SESSION_COOKIE_SECUREFalseTrueStops the cookie travelling over plain HTTP (ASVS V3.3.1)
SESSION_COOKIE_HTTPONLYTrueTrueKeeps the cookie away from JavaScript and XSS payloads (ASVS V3.3.4)
SESSION_COOKIE_SAMESITENone (unset)'Lax', or 'Strict' for admin appsBlocks the cookie on cross-site POSTs, a second CSRF layer (ASVS V3.3.2)
SESSION_COOKIE_NAMEsession__Host-session where no subdomain sharing is neededThe prefix forces Secure, Path=/ and no Domain attribute (ASVS V3.3.3)
SESSION_COOKIE_DOMAINNoneLeave unsetSetting it shares the cookie with every subdomain
PERMANENT_SESSION_LIFETIME31 daysHours, not weeks, for sensitive appsFlask rejects signatures older than this value, limiting replay
REMEMBER_COOKIE_SECURE (Flask-Login)FalseTrueThe remember cookie is a long-lived login credential
REMEMBER_COOKIE_DURATION (Flask-Login)365 days14 to 30 daysShortens the window for a stolen remember cookie
REMEMBER_COOKIE_SAMESITE (Flask-Login)None'Lax'Same reasoning as the session cookie

Session handling rules

  • Clear on login: call session.clear() before writing the user ID at login, as the Flask security documentation shows, so no attacker-planted values survive authentication. This is the Flask form of the session fixation defence in ASVS V7.2.4. See the Session Fixation Guide.
  • Logout is not revocation: with client-side sessions, clearing the cookie on logout does not invalidate a copy an attacker already holds. If you need server-side revocation (forced logout, account suspension, "log out all devices"), either move to a server-side session store such as the Flask-Session extension backed by Redis or a database, or store a per-user session version number in the database and compare it on every request.
  • Flask-Login session protection: Flask-Login defaults to session_protection = "basic", which hashes the IP address and user agent and marks the session non-fresh when they change. "strong" deletes the session instead. Use fresh_login_required on password change, MFA enrolment and payment views so a stolen cookie cannot reach them. Strong mode can log out users on mobile networks whose IP address changes often, so test it before enforcing.
  • Caching proxies: CVE-2023-30861 showed that a Flask response refreshing a permanent session could be cached by a proxy and served, with its Set-Cookie, to another user, because Flask set Vary: Cookie only when the session was accessed or modified. It is fixed in Flask 2.2.5 and 2.3.2. Beyond upgrading, mark authenticated responses Cache-Control: private or no-store and configure CDNs not to cache responses that carry Set-Cookie.

For the wider theft and replay picture, including cookie binding, see the Session Hijacking and Cookie Theft Guide.

CSRF, templates and security headers

CSRF with Flask-WTF

Any view that processes a FlaskForm gets CSRF validation, but views that read request.form or JSON directly do not. Register CSRFProtect(app) (or csrf.init_app(app) in an application factory) so every POST, PUT, PATCH and DELETE is checked, the default WTF_CSRF_METHODS. Key settings:

  • Forms without FlaskForm: render <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">.
  • JavaScript clients: expose the token with {{ csrf_meta_tag() }} and send it in the X-CSRFToken or X-CSRF-Token header (the default WTF_CSRF_HEADERS).
  • Referrer check: keep WTF_CSRF_SSL_STRICT True (the default), which checks that the referrer matches the host on HTTPS requests.
  • Token lifetime: WTF_CSRF_TIME_LIMIT defaults to 3,600 seconds; keep page cache lifetimes below it to avoid spurious failures.
  • Exemptions: @csrf.exempt is legitimate only for endpoints authenticated by something other than cookies, such as webhook receivers that verify an HMAC signature or APIs that require a bearer token. Review every exemption.

See the CSRF Prevention Guide for the underlying patterns.

Templates

Autoescaping covers HTML element content, but Flask's documentation lists the gaps: HTML generated outside Jinja, Markup() called on user input, unquoted attributes such as <input value={{ value }}>, and user-controlled href values that can carry a javascript: URL. Two further Flask-specific risks deserve a code search: render_template_string() called with a string built from user input, which turns template syntax into server-side template injection and can lead to code execution (ATT&CK T1059.006, Python), and templates named with extensions outside the autoescape list, such as .txt or .j2, which are rendered without escaping. See the Cross-Site Scripting (XSS) Prevention Guide.

Security headers

Flask sets none. Add them in an after_request handler, at the reverse proxy, or with an extension such as Flask-Talisman, which the Flask documentation mentions. The values below are the starting points the Flask documentation gives.

HeaderStarting valuePurpose
Strict-Transport-Securitymax-age=31536000; includeSubDomainsForces HTTPS for a year
Content-Security-Policydefault-src 'self', then refineLimits script sources and blocks javascript: URLs
X-Content-Type-OptionsnosniffStops MIME sniffing of uploaded content
X-Frame-OptionsSAMEORIGIN (or CSP frame-ancestors)Clickjacking defence

See the Content Security Policy Guide and the Clickjacking Guide.

Debug mode, error handling and production serving

The Flask and Werkzeug documentation could not be clearer: never run the development server or the debugger in production, where "production" means anything that is not local development and anything publicly reachable. The debugger's console executes arbitrary Python in the server process. Its PIN is described by Werkzeug as a helper that makes exploitation harder, not a security control, and it can be disabled entirely with WERKZEUG_DEBUG_PIN=off. An exposed debugger is an exploitable public-facing application (ATT&CK T1190) that hands over the process, its environment variables and therefore every secret the app holds.

  • Serve with a WSGI server: run under Gunicorn, Waitress, mod_wsgi or uWSGI behind a reverse proxy such as nginx, never flask run or app.run().
  • Guard debug in code: fail start-up if app.debug is True and the environment is not development. Search for debug=True, --debug and DebuggedApplication in the codebase, Dockerfiles and process manager configs.
  • Error handlers: register @app.errorhandler(500) and @app.errorhandler(Exception) that return a generic page or JSON body with a correlation ID, and log the traceback server-side (ASVS V16.5.1). Leave PROPAGATE_EXCEPTIONS and TRAP_BAD_REQUEST_ERRORS at their production defaults.
  • Logging hygiene: never log request bodies, Authorization headers or the session cookie. See the Secure Error Handling and Logging Guide.

Proxies, hosts and request limits

Most Flask deployments sit behind at least one proxy or load balancer, and this layer is where several quiet vulnerabilities live.

  • ProxyFix counts: Werkzeug's ProxyFix rewrites REMOTE_ADDR, the URL scheme and the host from X-Forwarded-For, -Proto, -Host, -Port and -Prefix. Set x_for, x_proto and x_host to the exact number of proxies that set each header, and only use it when there really is a proxy. Its own documentation warns that trusting values the client supplied is a security issue: an over-trusting ProxyFix lets anyone spoof their IP address past rate limits and audit logs (ASVS V4.1.3). See the Proxies and Forward vs Reverse Proxy Security Guide.
  • Trusted hosts: set TRUSTED_HOSTS (Flask 3.1) to your real host names, for example ["app.example.com", ".example.com"]. Without it, url_for(..., _external=True) builds links from whatever Host header arrives, which is how password reset emails end up pointing at attacker domains. Invalid hosts raise a SecurityError that returns 400.
  • Request size: set MAX_CONTENT_LENGTH, which is unset by default, and lower it per view through request.max_content_length. Keep the Flask 3.1 defaults for MAX_FORM_MEMORY_SIZE (500 kB per field) and MAX_FORM_PARTS (1,000). Mirror limits and timeouts at the WSGI server and proxy (for example nginx client_max_body_size and Gunicorn --timeout).
  • File handling: pass client file names through werkzeug.utils.secure_filename(), store uploads outside the static folder, serve them with send_from_directory(..., as_attachment=True) rather than building paths with string concatenation, and never serve uploaded HTML inline. See the Secure File Upload Handling Guide.
  • Redirects: validate any next parameter after login against a relative path or an allow list before calling redirect(), since Flask-Login's examples leave this check to the developer.

Choosing and vetting extensions

A Flask application's attack surface is largely the sum of its extensions. Each one runs with the application's full privileges and often reads SECRET_KEY.

NeedCommon choicesSecurity settings to check
CSRF and formsFlask-WTFCSRFProtect registered globally, WTF_CSRF_ENABLED not disabled outside tests, exemptions reviewed
Login sessionsFlask-LoginRemember cookie flags and duration, session_protection, fresh_login_required on sensitive views, safe next handling
Server-side sessionsFlask-SessionBackend authentication (Redis ACLs, TLS), session ID regeneration, expiry in the store
Headers and HTTPSFlask-Talisman or proxy configurationCSP policy content, HSTS max-age, no conflicting headers from the proxy
Rate limitingFlask-LimiterShared storage across workers, key function uses the ProxyFix-corrected address
CORSFlask-CORSNo wildcard origin with credentials; explicit allow list
DatabaseFlask-SQLAlchemyORM queries or bound parameters only; no text() with string formatting

Decision guidance. A small internal tool with a handful of users can live with client-side sessions, Flask-WTF and headers set at the proxy. A customer-facing or regulated application should move to server-side sessions for revocation, enforce fresh login on sensitive actions, and run CSP in report-only mode first. A pure JSON API consumed by non-browser clients should not use cookie sessions at all: authenticate with OAuth access tokens validated per request, and CSRF then largely falls away. See the API Security Guide. When choosing an extension, prefer actively maintained projects (recent releases, security advisories handled), pin versions with hashes, and scan dependencies with pip-audit in CI.

Implementation roadmap

  1. Inventory: list every Flask app, its Flask and Werkzeug versions, registered extensions, how SECRET_KEY is loaded, and how it is served (WSGI server, proxy chain).
  2. Remove critical exposures first: disable debug mode anywhere it is reachable, replace hard-coded or tutorial SECRET_KEY values (outright, not via fallbacks), and upgrade past known advisories such as CVE-2023-30861.
  3. Harden configuration: set the cookie flags, lifetimes, TRUSTED_HOSTS, MAX_CONTENT_LENGTH and correct ProxyFix counts in a shared base config used by every app.
  4. Add CSRF and headers: register CSRFProtect, then deploy CSP in Content-Security-Policy-Report-Only mode, collect reports for two to four weeks, and enforce.
  5. Test: add integration tests that assert cookie attributes, the 400 response for an unknown host, the 400 response for a missing CSRF token, and that error responses contain no traceback. Run a DAST scan against staging.
  6. Roll out and roll back: deploy to a canary instance first. Cookie changes (name, SameSite, Domain) log users out or break embedded flows, so keep the previous config as a feature flag and revert it if login or payment error rates rise. Never roll back debug mode or a leaked key.
  7. Automate: add Bandit (which flags debug=True), secret scanning on commits and built wheels, and pip-audit to the pipeline. See the Secure Software Development Lifecycle (SSDLC) Guide.

Detection and measurement

  • What to log: logins, logouts, failed logins, CSRF failures (Flask-WTF logs them at INFO to the flask_wtf.csrf logger), SecurityError host rejections, 413 responses, and session signature failures.
  • What to alert on: requests for /console or Werkzeug debugger resources, spikes in invalid session signatures (key guessing or a botched rotation), and a burst of CSRF failures from one origin.

Metrics that show the programme is working:

  1. Percentage of Flask apps with debug provably off in production (target 100%).
  2. Percentage of apps loading SECRET_KEY from a secrets manager, and days since last rotation.
  3. Percentage of apps passing an automated cookie flag and header check.
  4. Number of @csrf.exempt routes without a documented alternative authentication method (target zero).
  5. Mean time to upgrade Flask, Werkzeug and extensions after a security release.

Framework mapping

Control areaNIST SP 800-53 Rev. 5NIST CSF 2.0OWASP ASVS 5.0ISO/IEC 27001:2022 Annex A
Secret key managementIA-5, SC-12PR.AA-03V13.3.1A.8.24
Session and cookie securitySC-23PR.DS-02V3.3.1, V3.3.2, V3.3.4, V7.2.4A.8.26
Debug off, least functionalityCM-7PR.PS-01V13.4.2A.8.9
Input and request limitsSI-10, SC-5PR.PS-06V4.1.3A.8.28
Error handling and loggingSI-11, AU-2PR.PS-04V16.5.1A.8.15
Dependency and extension riskRA-5, SA-11ID.RA-01V13.4.6A.8.25

CIS Controls v8.1 Control 16 (Application Software Security) covers the development practices in this guide.

Common failure patterns

  • Tutorial secret keys in production: SECRET_KEY = "dev" or a value copied from documentation, committed to Git or shipped in a package. As the PyPI Secrets Exposure 2023 research showed, configuration files leak through published packages long after the source repository is cleaned.
  • Debug left on: app.run(debug=True) at the bottom of app.py, then the same file used as the container entry point.
  • Sensitive data in the session: roles, API tokens or personal data stored in a cookie the user can decode with a base64 tool.
  • Selective CSRF: FlaskForm used on some pages while AJAX endpoints and raw request.form handlers have no token check.
  • Blind proxy trust: ProxyFix(app, x_for=2) behind a single proxy, letting clients choose their own IP address for rate limiting and logs.
  • Assuming logout revokes: responding to a stolen account by asking the user to log out, while the attacker's copy of the signed cookie stays valid for 31 days.

Practitioner checklist

  • Generate SECRET_KEY with secrets.token_hex(), load it from a secrets manager, use one per environment, and plan rotation with SECRET_KEY_FALLBACKS.
  • Set SESSION_COOKIE_SECURE=True, SESSION_COOKIE_SAMESITE='Lax', keep HttpOnly, and shorten PERMANENT_SESSION_LIFETIME.
  • Apply the same flags to Flask-Login's remember cookie and cut REMEMBER_COOKIE_DURATION from 365 days.
  • Call session.clear() at login and use server-side sessions or a session version where revocation matters.
  • Register CSRFProtect globally and review every @csrf.exempt.
  • Prove debug mode is off in production and serve through a WSGI server behind a reverse proxy.
  • Set TRUSTED_HOSTS, MAX_CONTENT_LENGTH and exact ProxyFix counts.
  • Add HSTS, CSP, nosniff and frame protection, starting CSP in report-only mode.
  • Search for render_template_string, Markup( and |safe on user data.
  • Pin Flask, Werkzeug and extensions, and run pip-audit, Bandit and secret scanning in CI.

Standards and references

Related NHI Mgmt Group resources: Python Security Guide · Secrets Management Guide · Token and Session Security Guide · CSRF Prevention Guide · gRPC Security Guide

PDF for subscribers

Take this guide with you

Every section, table and checklist in one PDF, with links to the sources and related guides. Subscribe to our weekly NHI & AI Identity Journal and we’ll email you a download link.

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 26 October 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org

PDF Downloads for Newsletter Subscribers

Get the guide

Enter your details and we’ll email you a download link.

The full guide is free to read online. The PDF is a thank you for subscribing.

Fill in all fields marked * and tick the PDF box to continue.