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

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.

Flask
VersionLatestStatusReleasedEnd of support
3.13.1.3SupportedSupported until the next feature release.13 November 2024No date set
3.03.0.3End of lifeSuperseded by 3.1.30 September 202313 November 2024
CPython
VersionLatestStatusReleasedEnd of support
3.153.15.0Supported9 October 2026October 2031
3.143.14.8SupportedThe cards run 3.14.8.7 October 2025October 2030
3.133.13.16Security fixes only7 October 2024October 2029
3.123.12.15Security fixes only2 October 2023October 2028
3.113.11.17Security fixes only24 October 2022October 2027
3.103.10.22End of life4 October 20211 October 2026

Each Python version gets two years of bug fixes and then security fixes only, for five years in all. CPython release schedule

How the fix cards choose versions

Minimum secure versions

The lowest release in each supported line that fixes every published advisory listed here. Anything older is missing a security fix.

PackageAt leastWhy
flask3.1.3CVE-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
werkzeug3.1.9CVE-2026-102598, the latest of several safe_join() fixes for Windows device names (3.1.4 to 3.1.9). Advisory
jinja23.1.6CVE-2025-27516: sandbox breakout through the attr filter. Advisory
CPython3.15.0, 3.14.8, 3.13.16, 3.12.15 or 3.11.17Six 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

  1. Configuration: Load SECRET_KEY from the environment (with SECRET_KEY_FALLBACKS for rotation), the cookie flags, MAX_CONTENT_LENGTH and TRUSTED_HOSTS before registering blueprints.
  2. Proxy headers: Wrap app.wsgi_app in ProxyFix(app.wsgi_app, x_for=1, x_proto=1) only behind that many proxies, so request.remote_addr and the scheme come from your proxy.
  3. CSRF protection: CSRFProtect(app) from Flask-WTF before the blueprints, so every POST view is covered; exempt only views authenticated by a token.
  4. Security headers: Set CSP, HSTS, X-Content-Type-Options and frame protection in an after_request handler or at the reverse proxy.
  5. Blueprints and views: Authenticate with a decorator or before_request, and check object ownership in each view's query.
  6. Error handlers: Register handlers that return generic messages, and keep DEBUG off: the Werkzeug debugger runs code typed into the browser.

Secure defaults and settings

SettingValueWhy
SESSION_COOKIE_SECUREFalseSet True when served over HTTPS.
SESSION_COOKIE_SAMESITENoneSet 'Lax'.
SESSION_COOKIE_HTTPONLYTrueKeep it.
MAX_CONTENT_LENGTHNone (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_HOSTSNoneList your host names (new in 3.1) so a forged Host header is rejected.
Jinja autoescapingOn 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.

APIRiskInstead
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 inputServer-side template injection.Fixed templates; pass input only as context values.
send_file() with a path from the requestPath traversal.send_from_directory() with a fixed folder.
app.run(debug=True) or FLASK_DEBUG=1The interactive debugger runs code in the server.Debug only on a developer machine.
pickle.loads(), yaml.load() on request dataCode execution while deserializing.JSON with a schema, or yaml.safe_load().
@csrf.exemptRemoves 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 Markup or |safe.
  • v5.0.0-3.3.1 SESSION_COOKIE_SECURE is 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 DEBUG is 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.

References

  1. Flask: security considerations
  2. Flask: configuration handling
  3. Flask: tell Flask it is behind a proxy
  4. Pallets version support policy
  5. Python Developer's Guide: status of Python versions

All fix cards