Overview
The CVE-2021-21241 vulnerability affects Flask-Security-Too, an independently maintained fork of Flask-Security used to add authentication features to Flask apps. In versions 3.3.0 through 3.4.4, the /login and /change endpoints could return the authenticated user’s token in a response to a GET request. Because GET requests are typically not protected by CSRF tokens, a malicious third party could coax a victim’s browser to make that GET request and harvest the token from the response, enabling token theft and potential impersonation. This token leakage is a security misconfiguration in practice: token data is revealed in GET responses instead of being restricted to POST flows and properly protected endpoints. The official patches fix this by ensuring tokens are not exposed via GET and by tightening token handling in the library (patched in 3.4.5 and 4.0.0). As a workaround, if you aren’t using authentication tokens, you can set SECURITY_TOKEN_MAX_AGE to 0 to invalidate tokens immediately.
Affected Versions
3.3.0 through 3.4.4
Code Fix Example
Flask API Security Remediation
VULNERABLE PATTERN (GET exposes token):
from flask import Flask, jsonify
import os
app = Flask(__name__)
# Simulated in-app token for demonstration purposes
TOKENS = {"user1": "token-abc123"}
@app.route('/login', methods=['GET'])
def login_get():
# Vulnerable: returns token in GET response which is not CSRF-protected
user_id = 'user1'
token = TOKENS.get(user_id)
return jsonify({"token": token, "message": "Use POST to login"})
if __name__ == '__main__':
app.run(debug=True)
FIXED PATTERN (no token on GET; login uses POST):
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
# Simulated in-app token for demonstration purposes
TOKENS = {"user1": "token-abc123"}
@app.route('/login', methods=['POST'])
def login_post():
# Proper login flow via POST; token exposed only after successful authentication
data = request.get_json(silent=True) or {}
username = data.get('username')
password = data.get('password')
if username == 'user1' and password == 's3cr3t':
token = TOKENS.get('user1')
return jsonify({"token": token})
abort(401)
# Optionally keep a GET handler that does not leak tokens
@app.route('/login', methods=['GET'])
def login_get_safe():
abort(405) # disallow token exposure via GET
if __name__ == '__main__':
app.run(debug=True)