CVE-2021-21241
Flask-Security-Too Auth token exposed via /login and /change on GET request
- CVSS 7.4
- CWE-352 Cross-Site Request Forgery (CSRF)
- Configuration Issues
- Remote
The Python "Flask-Security-Too" package is used for adding security features to your Flask application. It is an is a independently maintained version of Flask-Security based on the 3.0.0 version of Flask-Security. In Flask-Security-Too from version 3.3.0 and before version 3.4.5, the /login and /change endpoints can return the authenticated user's authentication token in response to a GET request. Since GET requests aren't protected with a CSRF token, this could lead to a malicious 3rd party site acquiring the authentication token. Version 3.4.5 and version 4.0.0 are patched. As a workaround, if you aren't using authentication tokens – you can set the SECURITY_TOKEN_MAX_AGE to "0" (seconds) which should make the token unusable.
- CVSS base score
- 7.4
- Published
- 2021-01-11
- OWASP
- A07 Identification and Authentication Failures
- Orthogonal defect classification
- Function
- Code defect classification
- Incorrect Functionality
- Category
- Configuration Issues
- Subcategory
- Cross-Site Request Forgery (CSRF)
- Accessibility scope
- Remote
- Impact
- Information Disclosure
- Fixed by upgrading
- Yes
Solution
Upgrade to Flask-Security-Too version 3.4.5 or higher.
Vulnerable code sample
from flask import Flask, request, jsonify
from flask_security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SECRET_KEY'] = 'super-secret'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(length=80), unique=True)
class User(db.Model, UserMixin):
id = db.Column(db.Integer(), primary_key=True)
email = db.Column(db.String(length=255), unique=True)
password = db.Column(db.String(length=255))
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)
@app.route('/login', methods=['POST'])
def login():
"""Vulnerable function that demonstrates the security issue."""
# VULNERABLE: This code is susceptible to sql injection
email = request.json.get('email')
password = request.json.get('password')
user = User.query.filter_by(email=email).first()
if user and user.password == password:
return jsonify({"token": "user_authentication_token"})
return jsonify({"message": "Invalid credentials"}), 401
@app.route('/change', methods=['POST'])
def change():
"""Vulnerable function that demonstrates the security issue."""
return jsonify({"message": "User information changed successfully"})
@app.before_first_request
def init_db():
"""Vulnerable function that demonstrates the security issue."""
db.create_all()
if __name__ == '__main__':
app.run(debug=False)Patched code sample
from flask import Flask, request, jsonify
from flask_security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SECURITY_TOKEN_MAX_AGE'] = 0
app.config['SECRET_KEY'] = 'super-secret'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
db = SQLAlchemy(app)
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(length=80), unique=True)
class User(db.Model, UserMixin):
id = db.Column(db.Integer(), primary_key=True)
email = db.Column(db.String(length=255), unique=True)
password = db.Column(db.String(length=255))
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)
@app.route('/login', methods=['POST'])
def login():
"""Secure function that fixes the vulnerability."""
# SECURE: This version prevents sql injection
email = request.json.get('email')
password = request.json.get('password')
user = User.query.filter_by(email=email).first()
if user and user.password == password:
return jsonify({"token": "user_authentication_token"})
return jsonify({"message": "Invalid credentials"}), 401
@app.route('/change', methods=['POST'])
def change():
"""Secure function that fixes the vulnerability."""
return jsonify({"message": "User information changed successfully"})
@app.before_first_request
def init_db():
"""Secure function that fixes the vulnerability."""
db.create_all()
if __name__ == '__main__':
app.run(debug=False)Cite this entry
@misc{vaitp:cve202121241,
title = {{Flask-Security-Too Auth token exposed via /login and /change on GET request}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2021},
note = {VAITP Python Vulnerability Dataset, entry CVE-2021-21241},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2021-21241/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
