Rate Limiting Bypass in Flask with Basic Auth
Rate Limiting Bypass in Flask with Basic Auth — how this specific combination creates or exposes the vulnerability
Rate limiting is a control intended to reduce the impact of brute-force, credential stuffing, and denial-of-service attempts. In Flask, developers commonly implement rate limits using extensions such as Flask-Limiter, applying limits per route or across groups of routes. When Basic Authentication is used, the interaction between authentication and rate limiting can introduce a bypass pattern that is especially important to understand.
Consider a Flask route that protects a sensitive operation with HTTP Basic Auth but applies the rate limit after the authentication check. If the rate limit is keyed only by identifier (e.g., IP address or API key) and not by the authenticated principal, an attacker who knows or can guess valid credentials can exhaust the rate limit for that specific credential while remaining within the per-IP or per-key quota. Because the limit is not bound to the authenticated identity, the attacker can cycle through stolen or brute-forced credentials within the same IP or key, effectively bypassing the intended per-user protection.
Another common pattern is to apply rate limits to the login or token endpoint itself. If the limit is scoped only by IP and does not incorporate the username or realm, an attacker can perform many authentication attempts under a single username from a single IP. Basic Auth sends credentials on each request in an encoded header; however, if the rate limit does not factor the username into the key, an attacker who knows the username can saturate the allowed request count for that user while staying under the IP-based threshold. This enables online password guessing or brute-force attacks against the Basic Auth credential without triggering protections that would normally lock or slow the attacker down.
Flask applications that use route-specific decorators must also be cautious about precedence and ordering. If a global rate limit is defined but a more permissive limit is applied to a specific route that performs authentication, the more permissive limit can be leveraged to amplify abuse. For example, an endpoint that skips authentication for certain methods or users but still counts against a looser limit provides a vector for abuse. Because Basic Auth credentials are static until changed, an attacker who obtains a valid pair can repeatedly use that pair within the permissive window, undermining the effectiveness of broader rate limits.
Middleware or proxy configurations in front of Flask can also affect how limits are applied. If the proxy terminates TLS and forwards requests to Flask over HTTP, and the rate limit is applied at the proxy without passing the authenticated identity to the application, the Flask layer may see only anonymous requests. This disconnect means the application cannot enforce identity-aware limits, and the proxy-level limit may not account for the number of distinct authenticated sessions. As a result, an attacker can authenticate with different credentials while appearing to the Flask app as a stream of requests from the same proxied source, bypassing identity-based throttling.
To detect this class of issue, scanning should examine how rate limits are defined in relation to authentication boundaries, check whether limits incorporate the authenticated principal (such as username or user ID), and verify that limits are enforced consistently across the authentication flow. Tools that correlate spec definitions with runtime behavior can highlight mismatches where authentication and rate limiting do not align, which is especially relevant when Basic Auth is used without additional adaptive controls.
Basic Auth-Specific Remediation in Flask — concrete code fixes
Remediation centers on tying rate limits to the authenticated identity rather than only to IP or generic keys. Below are concrete, realistic examples that show how to implement this in Flask using Flask-Limiter and HTTP Basic Auth.
Example 1: Rate limit keyed by username after successful Basic Auth
from flask import Flask, request, jsonify
from flask_httpauth import HTTPBasicAuth
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
auth = HTTPBasicAuth()
# In-memory user store for example; use a secure backend in production
users = {
"alice": "secret1",
"bob": "secret2"
}
@auth.verify_password
def verify_password(username, password):
if username in users and users[username] == password:
return username
return None
# Key rate limits by authenticated username, falling back to IP for unauthenticated requests
def get_ratelimit_key():
current_user = auth.current_user()
if current_user:
return f"user:{current_user}"
return get_remote_address()
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["200 per day", "50 per hour"]
)
@app.route("/sensitive", methods=["GET"])
@auth.login_required
@limiter.limit("10 per minute", key_func=get_ratelimit_key)
def sensitive():
return jsonify({"status": "ok", "user": auth.current_user()})
if __name__ == "__main__":
app.run()
In this example, the key_func returns user:{username} for authenticated requests, ensuring that the limit is applied per user. Unauthenticated requests fall back to the IP address. This prevents an attacker from exhausting the quota for a single user by cycling credentials from the same IP.
Example 2: Separate strict limits on authentication endpoints
@app.route("/login", methods=["GET"])
@auth.login_required
@limiter.limit("5 per minute", key_func=lambda: auth.current_user() or get_remote_address())
def login():
# Normally login would not require auth; this illustrates endpoint-specific limits
return jsonify({"message": "login endpoint"})
@app.route("/login_form", methods=["POST"])
def login_form():
username = request.form.get("username")
# Apply stricter, username-aware limits on credential submission
limiter.check_limit(
lambda: f"login_attempt:{username}" if username else get_remote_address()
)
# Perform verification as shown above
return auth.authenticate(username, request.form.get("password"))
Here, the login submission endpoint uses a dynamic key that includes the username when available, preventing an attacker from trying many passwords under a single IP quota. The use of limiter.check_limit demonstrates explicit enforcement aligned with the authentication action.
Operational and architectural recommendations
- Ensure that the rate limit key includes the authenticated identity for protected routes; avoid relying solely on IP-based limits when credentials are used.
- Apply tighter limits to authentication and credential verification endpoints, incorporating the username into the limit key to mitigate online guessing.
- Review the ordering of decorators to confirm that the more restrictive limits apply to sensitive operations, and that broader limits do not inadvertently create a bypass path.
- Log authentication failures along with rate limit triggers to aid incident investigation and to detect patterns of credential probing.
These code-level changes align the rate limiting boundaries with the authentication boundaries, reducing the risk that Basic Auth credentials can be abused through limit bypass mechanisms.
Related CWEs: resourceConsumption
| CWE ID | Name | Severity |
|---|---|---|
| CWE-400 | Uncontrolled Resource Consumption | HIGH |
| CWE-770 | Allocation of Resources Without Limits | MEDIUM |
| CWE-799 | Improper Control of Interaction Frequency | MEDIUM |
| CWE-835 | Infinite Loop | HIGH |
| CWE-1050 | Excessive Platform Resource Consumption | MEDIUM |