Fix cards for Python and Flask
A01:2025 Broken Access Control
- Cross-site request forgery (CSRF)CWE-352
Register Flask-WTF's CSRFProtect(app). It requires the session's token on every POST, PUT, PATCH and DELETE: render {{ csrf_token() }} in a hidden csrf_token field, or send it from scripts in an X-CSRFToken header.
- Missing object-level authorization (IDOR and BOLA)CWE-862 · CWE-639 · CWE-863
Load the signed-in user from Flask's signed session in a before_request hook, never from request data, and add the same scope to every invoice query: tenant_id = :tenant AND (owner_id = :user OR :is_billing_admin).
- Open redirectCWE-601
Refuse backslashes, which browsers read as slashes and urllib.parse does not, and control characters; then split next with urlsplit: accept a path that starts with exactly one slash, or an https URL whose hostname is in an exact allowlist, with no port and no @.
- Path traversalCWE-22 · CWE-23 · CWE-36 · CWE-59
Resolve both paths and compare whole components: base = Path(base_dir).resolve(), target = (base / name).resolve(), and serve only if target.is_relative_to(base). resolve() collapses ../ and follows symlinks, and the check also catches absolute names, which replace base in the / operator.
- Server-side request forgery (SSRF)CWE-918
Attach the policy to the connection, not the hostname string.
A02:2025 Security Misconfiguration
- CORS misconfiguration on credentialed APIsCWE-942 · CWE-346
Register flask-cors for the API routes with exact origins from configuration and supports_credentials=True.
- XML external entity (XXE) processingCWE-611 · CWE-776
Parse untrusted XML with defusedxml, not lxml or the stdlib.
A04:2025 Cryptographic Failures
- Insecure randomness for security tokensCWE-330 · CWE-338
Generate the token with the secrets module: secrets.token_urlsafe(32) is 256 bits from the OS CSPRNG.
- Weak password hashingCWE-916 · CWE-759 · CWE-327
Use argon2-cffi's PasswordHasher with explicit parameters at or above OWASP's minimum, PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1), and store ph.hash(password).
A05:2025 Injection
- Code injectionCWE-94 · CWE-95
Never evaluate user input as code. Parse the expression with ast.parse and walk the tree, allowing only numeric literals, the supplied variables and arithmetic operators; refuse everything else. eval(expr, {"__builtins__": {}}, vars) is not a sandbox: attribute access still reaches any object.
- Cross-site scripting (XSS)CWE-79
Let Jinja autoescaping do the work: render {{ value }} with no |safe filter and no Markup() around user input, so & < > " and ' become entities in both text and quoted attributes.
- CSV formula injectionCWE-1236
User text in a CSV is attacker-controlled. If a cell starts with = + - @, a tab, CR, LF or NUL, prefix it with an apostrophe so a spreadsheet treats it as text, then write it with csv.writer, which escapes commas, quotes and newlines.
- LDAP filter injectionCWE-90
Escape every value that goes into an LDAP filter with ldap3.utils.conv.escape_filter_chars, which encodes the RFC 4515 metacharacters (* ( ) \ NUL) so they are matched as data, not parsed as syntax.
- NoSQL operator injectionCWE-943
Never let a request value reach a PyMongo filter as a dict: accept str only (isinstance(value, str)), compare with {"field": {"$eq": value}}, and check secrets such as reset tokens in code with hmac.compare_digest after looking the account up by email.
- OS command and argument injectionCWE-78 · CWE-88
Pass a list to subprocess.run with the default shell=False - subprocess.run([tool, '--', name]) - so no shell parses the arguments, and put '--' before any user operand so a leading dash cannot pose as an option.
- Server-side template injectionCWE-1336 · CWE-94 · CWE-95
Pass input as data, never as template text: render_template_string('Hello {{ name }}', name=name), not render_template_string('Hello ' + name).
- SQL injectionCWE-89
Keep values out of the SQL text: use ? placeholders and pass a tuple, as in db.execute(sql, (name, status)).
- XPath injectionCWE-643
Bind the input as XPath variables, never splice it into the expression. lxml's doc.xpath("//employee[name=$n and role=$r]/title/text()", n=name, r=role) matches the values as data, so quotes and operators in them are literal.
A06:2025 Insecure Design
- ClickjackingCWE-1021
Refuse framing in an HTTP response header, not in the page. Send Content-Security-Policy: frame-ancestors 'none' (which modern browsers honor over X-Frame-Options) plus X-Frame-Options: DENY for older browsers, on every page except one you mean to be embedded.
- Regular-expression denial of service (ReDoS)CWE-1333
A validation regex with a nested quantifier backtracks catastrophically on CPython's re: a 30-character string runs for tens of seconds and, while it holds the GIL, freezes the whole process.
- Unrestricted file uploadCWE-434 · CWE-646 · CWE-73 · CWE-770
Decide the type from the bytes, and never let the client pick the name.
A07:2025 Authentication Failures
- Hard-coded secretsCWE-798 · CWE-261
Read the key from os.environ when the app is created, where the deployment puts it from your secret store, and raise if it is missing or blank so the app never starts without it.
- Improper TLS certificate validationCWE-295 · CWE-297 · CWE-296
Never pass verify=False. Pass verify= the path of the partner's private CA bundle on every request: requests then checks the chain against that CA alone, the dates and the host name, and a per-request value cannot be replaced by REQUESTS_CA_BUNDLE.
- JWT validation in APIs (bearer tokens)CWE-347 · CWE-287
Verify every bearer token with PyJWT's jwt.decode(token, key, algorithms=["RS256"], issuer=..., audience=..., leeway=30, options={"require": ["exp", "iss", "aud", "sub"]}), with the key from configuration.
- Missing authentication for a critical functionCWE-306
Authenticate the operator, do not trust the network or a client-supplied header.
- Session fixation and session cookiesCWE-384 · CWE-613 · CWE-614 · CWE-1004 · CWE-1275
With server-side sessions (Flask-Session), call app.session_interface.regenerate(session) after storing the user at sign-in; session.clear() keeps the same ID.
A08:2025 Software or Data Integrity Failures
- Insecure deserializationCWE-502
Do not rebuild objects from the token. Decode it as JSON and copy only the fields you expect into a fixed shape; pickle.loads runs code chosen by the bytes.
- Mass assignment (over-posting)CWE-915
Never copy request.get_json() onto a record with dict.update(), a setattr loop or Model(**data).
A09:2025 Security Logging and Alerting Failures
- CRLF injection in logs and response headersCWE-117 · CWE-93 · CWE-113
Log events as structured data: pass request values to the logger as fields and format each record as one JSON object, so CR, LF and escape characters are encoded and cannot end it.
Not covered in Python yet
These weaknesses have fix cards in other stacks but not in this one.
Supported versions
Pallets fixes bugs and security issues only in the latest feature release of each project; a security fix may be backported one version on request. Flask support policy.
| Version | Latest | Status | Released | End of support |
|---|---|---|---|---|
| 3.1 | 3.1.3 | SupportedSupported until the next feature release. | 13 November 2024 | No date set |
| 3.0 | 3.0.3 | End of lifeSuperseded by 3.1. | 30 September 2023 | 13 November 2024 |
| Version | Latest | Status | Released | End of support |
|---|---|---|---|---|
| 3.15 | 3.15.0 | Supported | 9 October 2026 | October 2031 |
| 3.14 | 3.14.8 | SupportedThe cards run 3.14.8. | 7 October 2025 | October 2030 |
| 3.13 | 3.13.16 | Security fixes only | 7 October 2024 | October 2029 |
| 3.12 | 3.12.15 | Security fixes only | 2 October 2023 | October 2028 |
| 3.11 | 3.11.17 | Security fixes only | 24 October 2022 | October 2027 |
| 3.10 | 3.10.22 | End of life | 4 October 2021 | 1 October 2026 |
Each Python version gets two years of bug fixes and then security fixes only, for five years in all. CPython release schedule
Minimum secure versions
The lowest release in each supported line that fixes every published advisory listed here. Anything older is missing a security fix.
| Package | At least | Why |
|---|---|---|
flask | 3.1.3 | CVE-2026-27205: the session did not add Vary: Cookie when accessed in some ways, so a cache could serve one user's page to another. Advisory |
werkzeug | 3.1.9 | CVE-2026-102598, the latest of several safe_join() fixes for Windows device names (3.1.4 to 3.1.9). Advisory |
jinja2 | 3.1.6 | CVE-2025-27516: sandbox breakout through the attr filter. Advisory |
CPython | 3.15.0, 3.14.8, 3.13.16, 3.12.15 or 3.11.17 | Six CVEs fixed on 30 September and 1 October 2026, such as an ssl use-after-free (CVE-2026-19445), a tarfile filter bypass (CVE-2026-19672) and unbounded zipfile decompression (CVE-2026-15310); 3.11 to 3.13 also fix two more tarfile CVEs, and the fix for one of them, CVE-2026-87910, was in no 3.14 release on 11 October 2026. Advisory |
Middleware and security order
- Configuration: Load
SECRET_KEYfrom the environment (withSECRET_KEY_FALLBACKSfor rotation), the cookie flags,MAX_CONTENT_LENGTHandTRUSTED_HOSTSbefore registering blueprints. - Proxy headers: Wrap
app.wsgi_appinProxyFix(app.wsgi_app, x_for=1, x_proto=1)only behind that many proxies, sorequest.remote_addrand the scheme come from your proxy. - CSRF protection:
CSRFProtect(app)from Flask-WTF before the blueprints, so every POST view is covered; exempt only views authenticated by a token. - Security headers: Set CSP, HSTS,
X-Content-Type-Optionsand frame protection in anafter_requesthandler or at the reverse proxy. - Blueprints and views: Authenticate with a decorator or
before_request, and check object ownership in each view's query. - Error handlers: Register handlers that return generic messages, and keep
DEBUGoff: the Werkzeug debugger runs code typed into the browser.
Secure defaults and settings
| Setting | Value | Why |
|---|---|---|
SESSION_COOKIE_SECURE | False | Set True when served over HTTPS. |
SESSION_COOKIE_SAMESITE | None | Set 'Lax'. |
SESSION_COOKIE_HTTPONLY | True | Keep it. |
MAX_CONTENT_LENGTH | None (no limit) | Set a limit; Flask 3.1 also limits non-file form fields to 500,000 bytes (MAX_FORM_MEMORY_SIZE) and multipart bodies to 1,000 parts (MAX_FORM_PARTS). |
TRUSTED_HOSTS | None | List your host names (new in 3.1) so a forged Host header is rejected. |
| Jinja autoescaping | On for .html, .htm, .xml, .xhtml and .svg templates, and for render_template_string() | Templates with other extensions are not escaped, and a template string built from input is template injection whatever the escaping. |
Escape hatches to search for
Framework APIs that switch a protection off. Each one is sometimes needed; search for them in review and make each use justify itself.
| API | Risk | Instead |
|---|---|---|
Markup(), |safe, {% autoescape false %} | Turns off escaping for that value or block. | Escaped output, or a sanitizer such as nh3 for HTML you must render. |
render_template_string() with user input | Server-side template injection. | Fixed templates; pass input only as context values. |
send_file() with a path from the request | Path traversal. | send_from_directory() with a fixed folder. |
app.run(debug=True) or FLASK_DEBUG=1 | The interactive debugger runs code in the server. | Debug only on a developer machine. |
pickle.loads(), yaml.load() on request data | Code execution while deserializing. | JSON with a schema, or yaml.safe_load(). |
@csrf.exempt | Removes CSRF protection from a cookie-authenticated view. | Exempt only views that authenticate with a token. |
Checklist, mapped to ASVS 5.0.0
- v5.0.0-1.2.1 Output goes through Jinja's autoescaping; no request data reaches
Markupor|safe. - v5.0.0-3.3.1
SESSION_COOKIE_SECUREis on. - v5.0.0-3.5.1 Every state-changing form or request carries a Flask-WTF CSRF token.
- v5.0.0-13.4.2
DEBUGis off and the debugger is unreachable in production. - v5.0.0-1.2.4 Queries use placeholders or the ORM; nothing is built with f-strings.
- v5.0.0-5.3.2 Files are served with
send_from_directory()from a fixed folder. - v5.0.0-16.5.1 Error handlers return generic messages.
- v5.0.0-15.2.1 Flask, Werkzeug and Jinja are at or above the minimum secure versions above.