Password Spraying in Flask with Bearer Tokens
Password Spraying in Flask with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Password spraying is an authentication technique where an attacker uses a small number of common passwords across many accounts to avoid triggering account lockouts. In Flask APIs that rely on Bearer Tokens for authentication, password spraying can be especially dangerous when the API exposes user enumeration or does not enforce adequate rate limiting on the token validation or login endpoints.
Consider a Flask application that uses bearer token authentication with a route like the following:
from flask import Flask, request, jsonify
app = Flask(__name__)
# Simulated user store
USERS = {
"alice": {"password": "Password123!", "role": "user"},
"admin": {"password": "SecurePass!99", "role": "admin"},
}
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
username = data.get('username')
password = data.get('password')
user = USERS.get(username)
if user and user['password'] == password:
# In real apps, generate a proper JWT or opaque token
token = f"token_{username}"
return jsonify({"access_token": token, "token_type": "bearer"}), 200
return jsonify({"error": "Invalid credentials"}), 401
@app.route('/protected', methods=['GET'])
def protected():
auth = request.headers.get('Authorization')
if not auth or not auth.startswith('Bearer '):
return jsonify({"error": "Unauthorized"}), 401
token = auth.split(' ')[1]
# naive token validation for example
if token in [f"token_{u}" for u in USERS]:
return jsonify({"data": "secret"}), 200
return jsonify({"error": "Forbidden"}), 403
if __name__ == '__main__':
app.run(debug=False)
In this example, the /login endpoint does not enforce per-username delays or global rate limits. An attacker can perform password spraying by iterating over a list of common passwords for a single username or by cycling through many usernames with a default password such as Password123!. Because the endpoint returns distinct responses for missing users (401) versus incorrect passwords (also 401), an attacker can infer whether a username exists, further aiding the spray. If the application reuses or poorly scopes Bearer tokens, compromised credentials from a successful spray can lead to unauthorized access to protected resources.
The presence of Bearer Tokens does not inherently prevent password spraying; if the token issuance depends only on username/password validation without additional mitigations, attackers can systematically test credentials across accounts. Lack of account lockout, absence of multi-factor authentication, and missing anomaly detection amplify the risk. Moreover, if token validation endpoints or introspection mechanisms are unauthenticated or weakly guarded, attackers might probe for token validity without being rate-limited, effectively turning the API into a password spraying vector.
Bearer Tokens-Specific Remediation in Flask — concrete code fixes
To mitigate password spraying in Flask when using Bearer Tokens, implement rate limiting, account lockout, and secure token handling. Below are concrete code examples that demonstrate these controls.
1. Add rate limiting per username and globally
Use Flask-Limiter to restrict login attempts:
from flask import Flask, request, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
# Per-username rate limit using a custom key function
def username_key():
return request.get_json().get('username', get_remote_address())
login_limiter = Limiter(
app=app,
key_func=username_key,
default_limits=["5 per minute"]
)
USERS = {
"alice": {"password": "Password123!", "role": "user"},
"admin": {"password": "SecurePass!99", "role": "admin"},
}
@app.route('/login', methods=['POST'])
@login_limiter.limit("3 per minute")
def login():
data = request.get_json()
username = data.get('username')
password = data.get('password')
user = USERS.get(username)
if user and user['password'] == password:
token = f"token_{username}"
return jsonify({"access_token": token, "token_type": "bearer"}), 200
return jsonify({"error": "Invalid credentials"}), 401
2. Use secure token generation and avoid leaking user existence
Return a generic response and use a constant-time comparison where applicable. Do not reveal whether a username exists:
import secrets
import hashlib
def generate_token(username: str) -> str:
# Use a secure random component and hash to avoid predictable tokens
random = secrets.token_hex(16)
combined = f"{username}:{random}"
return hashlib.sha256(combined.encode()).hexdigest()
USERS = {
"alice": {"password": "Password123!", "role": "user", "token_secret": "somesecret"},
"admin": {"password": "SecurePass!99", "role": "admin", "token_secret": "somesecret"},
}
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
username = data.get('username')
password = data.get('password')
user = USERS.get(username)
# Always perform a dummy check to keep timing similar
dummy_user = USERS.get('dummy')
if user and user['password'] == password:
token = generate_token(user['token_secret'])
return jsonify({"access_token": token, "token_type": "bearer"}), 200
# Even on failure, return same status and similar processing time
if dummy_user:
generate_token(dummy_user['token_secret'])
return jsonify({"error": "Invalid credentials"}), 401
3. Protect token introspection and revocation endpoints
If you expose an endpoint to validate or revoke tokens, ensure it requires proper authentication and is rate-limited:
@app.route('/introspect', methods=['POST'])
def introspect():
auth = request.headers.get('Authorization')
if not auth or not auth.startswith('Bearer '):
return jsonify({"active": False}), 401
token = auth.split(' ')[1]
# Perform token validation logic here, with rate limiting applied
# For example, check against a token store
return jsonify({"active": True, "username": "alice"}), 200
By combining these practices—rate limiting, secure token generation, and consistent response behavior—you reduce the effectiveness of password spraying attacks against Flask APIs that use Bearer Tokens.