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)