Security review / vulnerability hunting in existing Flask code (passive “notice issues while working” and active “scan the repo and report findings”).
It is intentionally written as a set of normative requirements (“MUST/SHOULD/MAY”) plus audit rules (what bad patterns look like, how to detect them, and how to fix/mitigate them).
MUST NOT request, output, log, or commit secrets (API keys, passwords, private keys, session cookies, SECRET_KEY).
MUST NOT “fix” security by disabling protections (e.g., turning off CSRF, relaxing CORS, disabling escaping, disabling auth checks).
MUST provide evidence-based findings during audits: cite file paths, code snippets, and configuration values that justify the claim.
MUST treat uncertainty honestly: if a protection might exist in infrastructure (reverse proxy, WAF, CDN), report it as “not visible in app code; verify at runtime/config”.
When asked to write new Flask code or modify existing code:
MUST follow every MUST requirement in this spec.
SHOULD follow every SHOULD requirement unless the user explicitly says otherwise.
MUST prefer safe-by-default APIs and proven libraries over custom security code.
MUST avoid introducing new risky sinks (template rendering from strings, shell execution, dynamic imports, unsafe redirects, serving user files as HTML, etc.).
A request is state-changing if it can create/update/delete data, change auth/session state, trigger side effects (purchase, email send, webhook send), or initiate privileged actions.
SHOULD use an app factory and environment-based config so production config is not hard-coded.
Example skeleton (illustrative; adjust to your project):
Load config from environment / secret store.
Fail closed if critical settings are missing in production.
Key baseline config targets:
SECRET_KEY set and not committed
SESSION_COOKIE_SECURE=True (when HTTPS) IMPORTANT NOTE: Only set Secure in production environment when TLS is configured. When running in a local dev environment over HTTP, do not set Secure property on cookies. You should do this conditionally based on if the app is running in production mode. You should also include a property like SESSION_COOKIE_SECURE which can be used to disable Secure cookies when testing over HTTP.
SESSION_COOKIE_HTTPONLY=True
SESSION_COOKIE_SAMESITE='Lax' (or 'Strict' if compatible)
TRUSTED_HOSTS set in production
Security headers set (CSP, etc.) either in app or at the edge
Severity: High (Critical if missing in production with sessions or signing)
Required:
MUST set a strong random SECRET_KEY in production.
MUST keep SECRET_KEY out of source control and out of logs.
MAY rotate keys periodically; MAY use SECRET_KEY_FALLBACKS to support rotation without instantly invalidating existing sessions, then remove old keys after the rotation window. This likely is not needed for smaller applications but is good practice for larger applications. As this may complicate deployment, suggest that it be implemented rather than implementing it by default.
Insecure patterns:
Missing SECRET_KEY in production.
Hard-coded SECRET_KEY in repo (including test keys accidentally used in prod).
Logging or printing SECRET_KEY.
Detection hints:
Search for SECRET_KEY =, app.secret_key =, SECRET_KEY_FALLBACKS =.
Check .env files committed to repo.
Check config modules for constants.
Fix:
Load from secret manager or environment variable.
Add a rotation process:
Set new SECRET_KEY
Keep old key(s) temporarily in SECRET_KEY_FALLBACKS
Remove old key(s) after the safe window.
Notes:
If the application uses Flask sessions (cookie-based by default), SECRET_KEY is directly security-critical.
MUST set SESSION_COOKIE_SECURE=True (cookies only over HTTPS). NOTE: Only set Secure in production environment when TLS is configured. When running in a local dev environment over HTTP, do not set Secure property on cookies. You should do this conditionally based on if the app is running in production mode. You should also include a property like SESSION_COOKIE_SECURE which can be used to disable Secure cookies when testing over HTTP.
MUST ensure SESSION_COOKIE_HTTPONLY=True (protect from JS access).
SHOULD set SESSION_COOKIE_SAMESITE='Lax' (recommended) or 'Strict' if compatible with UX.
SHOULD keep SESSION_COOKIE_DOMAIN=None unless you explicitly need subdomain-wide cookies.
If you need embedded/iframe third-party usage, MAY consider SESSION_COOKIE_PARTITIONED=True (requires HTTPS).
Insecure patterns:
SESSION_COOKIE_SECURE=False in production.
SESSION_COOKIE_HTTPONLY=False.
SESSION_COOKIE_SAMESITE=None with cookie-authenticated state-changing endpoints (higher CSRF risk).
Detection hints:
Inspect app.config.update(...) blocks and config classes.
Look for set_cookie(..., secure=..., httponly=..., samesite=...) usage on non-session cookies too.
Fix:
Set these config values explicitly in production config.
Notes:
SameSite is defense-in-depth; do not treat it as a full replacement for CSRF tokens.
IMPORTANT NOTE: If cookies are not being used for auth (ie auth is via Authentication header or other passed token), then there is no CSRF risk.
Required:
MUST protect all state-changing endpoints (POST/PUT/PATCH/DELETE) that rely on cookies for authentication.
MAY use a well-tested CSRF library/integration (form framework or middleware) rather than rolling your own.
MAY use additional defenses (Origin/Referer checking, SameSite cookies, Fetch Metadata headers, custom headers for AJAX/API), but tokens remain the primary defense for cookie-authenticated apps.
If tokens are impractical, or for small applications:
MUST at a minimum require a custom header to be set and set the session cookie SESSION_COOKIE_SAMESITE=lax, as this is the strongest method besides requiring a form token, and may be much easier to implement.
Insecure patterns:
Cookie-authenticated endpoints that change state with no CSRF protection.
Using GET for state-changing actions (amplifies CSRF risk).
Detection hints:
Enumerate routes with methods other than GET and identify auth mechanism.
Look for CSRF integrations (e.g., Flask-WTF, global CSRF middleware). If absent, treat as suspicious.
Check JSON API endpoints too, not only HTML forms.
Fix:
Add CSRF protection to all state-changing requests.
If the app is a pure API and uses Authorization headers (bearer tokens) rather than cookies, document that choice and ensure cookies aren’t used for auth. If cookies are not used for auth, there is no CSRF risk.
Notes:
XSS can defeat CSRF protections; CSRF defenses do not replace XSS prevention.
MUST rely on Jinja auto-escaping for HTML templates.
MUST NOT mark untrusted content as safe:
Avoid Markup(...) on user data.
Avoid Jinja |safe on user-controlled content.
MUST quote HTML attributes containing Jinja expressions (value="{{ x }}" not value={{ x }}).
MUST NOT serve uploaded HTML as active HTML; serve as download (Content-Disposition: attachment) or transform to a safe format. Note: This is only relevant if it is possible to upload document content such as html, js, css, etc. If it purely is image files, there is no concern.
SHOULD deploy a Content Security Policy (CSP) to mitigate XSS classes (including javascript: in href).
Insecure patterns:
Markup(request.args.get(...))
Template filters: {{ user_html|safe }}
Unquoted attributes in templates
Serving user-uploaded content directly with text/html or inline rendering
Detection hints:
Search for Markup( and investigate origin of the data.
Search template files for |safe, |tojson misuse, and unquoted attributes.
Review file-serving routes that might return user uploads without as_attachment=True. Note: This is only relevant if it is possible to upload document content such as html, js, css, etc. If it purely is image files, there is no concern.
Fix:
Remove unsafe marking; sanitize only when strictly necessary using a trusted HTML sanitizer.
Clickjacking protection (X-Frame-Options: SAMEORIGIN and/or CSP frame-ancestors) (there may be cases where the user wants to iframe their site elsewhere. If that is the case, work with them to safely allow it)
SHOULD consider additional hardening headers depending on app (Referrer-Policy, Permissions-Policy).
MUST ensure cookies are set with secure attributes (see FLASK-SESS-001).
NOTE: Security headers may be set via a proxy or other cloud provider. Check to see if there is evidence of that.
Insecure patterns:
No security headers anywhere (app or edge).
CSP missing on apps that display untrusted content.
Detection hints:
Search for after_request hooks, Flask-Talisman usage, reverse proxy config.
If not visible in app code, flag as “verify at edge”.
Fix:
Set headers centrally (middleware / after_request) or via reverse proxy/CDN.
Keep CSP realistic and compatible; avoid unsafe-inline where possible.
MUST avoid executing shell commands with untrusted input.
If subprocess is necessary:
MUST pass args as a list (not a string)
MUST NOT use shell=True with attacker-influenced strings
SHOULD use strict allowlists for any variable component
If possible, use pure python or a python library rather than using a subprocess or system command
Do not assume that arguments to commands will be inherently safe even in shell=False. Commands may incorrectly process these arguments as command line flags or other trusted values.
Insecure patterns:
os.system(user_input)
subprocess.run(f"cmd {user}", shell=True)
Passing user strings into bash -c, sh -c, PowerShell, etc.
Detection hints:
Search for os.system, subprocess, Popen, shell=True.
Trace data from request/DB into these calls.
Fix:
Use library APIs instead of shell commands.
If unavoidable, hard-code the command and allowlist validated parameters. If supported by the subcommand, try to keep user values after -- to prevent them being processed as command line flags.
Note: For small stand alone projects this is less important. It is most important when deploying into an LAN or with other services listening on the same server.
Required:
MUST treat outbound requests to user-provided URLs as high risk.
SHOULD validate and restrict destinations (allowlist hosts/domains) for any user-influenced URL fetch.
SHOULD block access to:
localhost / private IP ranges / link-local addresses
cloud metadata endpoints
MUST NOT allow non http/https protocols (ie file: etc)
SHOULD set timeouts and restrict redirects.
Insecure patterns:
requests.get(request.args["url"])
Webhooks/preview/fetch endpoints that accept arbitrary URLs.
Detection hints:
Search for requests.get/post, httpx, urllib, aiohttp usage with untrusted URL sources.
Identify URL fetch features (preview, import, webhook tester).
Fix:
Ensure URLs are http or https (disallow file: or other protocols)
Enforce allowlists and network egress controls.
Add strict parsing and IP resolution checks; set timeouts; disable redirects if not needed.
SHOULD pin and regularly update security-critical dependencies (Flask, Werkzeug, Jinja2, itsdangerous).
MUST respond to known security advisories promptly.
Audit focus example:
If running on Windows and using file serving with untrusted paths, ensure Werkzeug’s safe_join behavior is not vulnerable to Windows device-name edge cases.
Detection hints:
Check requirements.txt, lockfiles, and runtime environments.
Identify where security helpers are used (safe_join, send_from_directory).
Fix:
Upgrade to patched versions and add regression tests for the impacted behavior.