HIGH password sprayingflaskbearer tokens

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.

Frequently Asked Questions

Can password spraying be detected by middleBrick scans?
middleBrick scans test authentication mechanisms and rate limiting as part of its 12 security checks. If your Flask API lacks per-username rate limits or exposes user enumeration, findings will be reported with remediation guidance.
Does using Bearer Tokens eliminate the need for account lockout policies?
No. Bearer Tokens protect the API after authentication, but weak authentication endpoints remain vulnerable to password spraying. Account lockout, rate limiting, and secure token handling are still necessary.