Improper Restriction of Excessive Authentication Attempts
This weakness occurs when a system allows an attacker to make unlimited login attempts without penalty. Without rate limiting or account lockouts, an attacker…
This weakness occurs when a system allows an attacker to make unlimited login attempts without penalty. Without rate limiting or account lockouts, an attacker can systematically guess passwords or usernames until they find valid credentials. This is one of the most common and easiest-to-exploit authentication flaws.
02How It Happens
Authentication systems typically accept a username and password, verify them against stored credentials, and grant access if they match. The vulnerability arises when the application does not track or limit the number of failed attempts from a single user, IP address, or account. An attacker can write a simple script to submit hundreds or thousands of login requests per second, testing common passwords or dictionary words. Without throttling, lockouts, or CAPTCHA challenges, the attacker will eventually succeed against weak or reused passwords.
03Real-World Impact
Successful brute-force attacks lead to unauthorized account access, allowing attackers to steal sensitive data, modify account settings, impersonate users, or pivot to other systems. For administrative accounts, compromise can result in full site takeover, malware injection, or data exfiltration. Even if most user passwords are strong, a small percentage of weak passwords in a large user base provides an easy entry point.
04Vulnerable & Fixed Patterns
Vulnerable pattern
from flask import Flask, request, session
import sqlite3
app = Flask(__name__)
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username')
password = request.form.get('password')
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('SELECT * FROM users WHERE username = ? AND password = ?',
(username, password))
user = cursor.fetchone()
if user:
session['user_id'] = user[0]
return 'Login successful'
else:
return 'Invalid credentials', 401
Why it's vulnerable: The endpoint accepts login attempts without tracking failed attempts, implementing delays, or locking accounts. An attacker can submit requests as fast as the server processes them.
Fixed pattern
from flask import Flask, request, session
import sqlite3
import time
from functools import wraps
app = Flask(__name__)
app.config['SESSION_COOKIE_SECURE'] = True
# Simple in-memory rate limiter (use Redis in production)
failed_attempts = {}
def rate_limit_login(f):
@wraps(f)
def decorated_function(*args, **kwargs):
username = request.form.get('username', '')
ip_addr = request.remote_addr
key = f"{username}:{ip_addr}"
if key in failed_attempts:
attempts, last_time = failed_attempts[key]
if attempts >= 5 and time.time() - last_time < 900: # 15 min lockout
return 'Account temporarily locked', 429
return f(*args, **kwargs)
return decorated_function
@app.route('/login', methods=['POST'])
@rate_limit_login
def login():
username = request.form.get('username')
password = request.form.get('password')
ip_addr = request.remote_addr
key = f"{username}:{ip_addr}"
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('SELECT id FROM users WHERE username = ? AND password_hash = ?',
(username, hash_password(password)))
user = cursor.fetchone()
if user:
failed_attempts.pop(key, None)
session['user_id'] = user[0]
return 'Login successful'
else:
if key not in failed_attempts:
failed_attempts[key] = (1, time.time())
else:
attempts, _ = failed_attempts[key]
failed_attempts[key] = (attempts + 1, time.time())
return 'Invalid credentials', 401
Vulnerable pattern
<?php
session_start();
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$conn = new mysqli('localhost', 'user', 'pass', 'myapp');
$result = $conn->query("SELECT id FROM users WHERE username = '$username' AND password = '$password'");
if ($result->num_rows > 0) {
$_SESSION['user_id'] = $result->fetch_assoc()['id'];
echo 'Login successful';
} else {
http_response_code(401);
echo 'Invalid credentials';
}
?>
Why it's vulnerable: No tracking of failed attempts, no account lockout, and no delay between requests. An attacker can submit unlimited login attempts in rapid succession.
Implement account lockout after a configurable number of failed attempts (typically 5–10), with a time-based or manual unlock mechanism.
Track failed login attempts by both username and source IP address to prevent distributed attacks and account enumeration.
Introduce progressive delays between failed attempts (e.g., 1 second after the 3rd attempt, 5 seconds after the 5th) to slow brute-force scanning.
Use CAPTCHA or multi-factor authentication (MFA) after a threshold of failed attempts to add friction without permanently locking legitimate users.
Log all authentication attempts (successful and failed) with timestamps and source IPs for monitoring and incident response.
Use a dedicated rate-limiting library or service (e.g., Redis, AWS WAF) in production rather than in-memory tracking.
06Signs You May Already Be Affected
Monitor your authentication logs for patterns of repeated failed login attempts from a single IP address or against a single account over a short time window. Unexpected successful logins from unfamiliar IP addresses or at unusual times may indicate a compromised account. Check for any admin or service accounts you do not recognize, which could indicate an attacker gained access.