Broken Authentication

Broken Authentication in Flask and Flask-Security-Too [CVE-2021-21241]

[Updated month year] Updated CVE-2021-21241

Overview

The Flask ecosystem has had real-world vulnerability exposure around authentication tokens in the Flask-Security-Too package. CVE-2021-21241 describes an issue where Flask-Security-Too versions 3.3.0 through 3.4.4 could return an authenticated user's token in response to GET requests to /login and /change. Because GET requests are not CSRF-protected by default, this could allow a malicious third party site to obtain and reuse tokens, leading to session hijacking or account compromise. This pattern maps to CWE-352: Cross-Site Request Forgery (CSRF) countermeasures not properly applied to sensitive authentication flows. The publicly patched versions are 3.4.5 and 4.0.0, which fix token leakage on GET responses. As an interim workaround, if you are not using authentication tokens, you can set SECURITY_TOKEN_MAX_AGE to 0 to render tokens unusable. In real Flask apps, this vulnerability manifests when token-bearing responses are exposed to cross-origin GET requests, bypassing typical CSRF protections and enabling token leakage.

Affected Versions

Flask-Security-Too 3.3.0-3.4.4 (patched in 3.4.5 and 4.0.0)

Code Fix Example

Flask API Security Remediation
VULNERABLE (GET returns token):
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/login', methods=['GET', 'POST'])
def login():
    # Vulnerable: token may be returned on GET
    if app.request.method == 'GET':
        token = 'dummy-token-please-replace'
        return jsonify({'token': token})
    # normal login flow on POST (simplified)
    return jsonify({'status': 'logged_in'})

if __name__ == '__main__':
    app.run(debug=True)

FIXED (POST-only with CSRF, token issuance):
from flask import Flask, jsonify, request
from flask_wtf import CSRFProtect

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-very-secret-key'
csrf = CSRFProtect(app)

# Simple in-memory user store for demonstration
USERS = {'alice': 'password123'}

def authenticate(username, password):
    return USERS.get(username) == password

def issue_token(username):
    # In real apps, use a secure serializer or token service
    return f"token-{username}-signed"

@app.route('/login', methods=['POST'])
def login_fixed():
    username = request.form.get('username')
    password = request.form.get('password')
    if not authenticate(username, password):
        return jsonify({'error': 'invalid_credentials'}), 401
    token = issue_token(username)
    return jsonify({'token': token})

if __name__ == '__main__':
    app.run(debug=True)

CVE References

Choose which optional cookies to allow. You can change this any time.