Weakness reference
CWE-307

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…

01Summary

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.

Fixed pattern
<?php
session_start();

$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$ip_addr = $_SERVER['REMOTE_ADDR'];

$conn = new mysqli('localhost', 'user', 'pass', 'myapp');

// Check for rate limit
$key = hash('sha256', $username . ':' . $ip_addr);
$result = $conn->query("SELECT attempts, last_attempt FROM login_attempts WHERE attempt_key = '$key'");

if ($result->num_rows > 0) {
    $row = $result->fetch_assoc();
    if ($row['attempts'] >= 5 && (time() - $row['last_attempt']) < 900) {
        http_response_code(429);
        echo 'Account temporarily locked';
        exit;
    }
}

// Verify credentials
$stmt = $conn->prepare("SELECT id FROM users WHERE username = ? AND password_hash = ?");
$stmt->bind_param('ss', $username, password_hash($password, PASSWORD_BCRYPT));
$stmt->execute();
$result = $stmt->get_result();

if ($result->num_rows > 0) {
    $_SESSION['user_id'] = $result->fetch_assoc()['id'];
    $conn->query("DELETE FROM login_attempts WHERE attempt_key = '$key'");
    echo 'Login successful';
} else {
    // Log failed attempt
    $conn->query("INSERT INTO login_attempts (attempt_key, attempts, last_attempt) 
                  VALUES ('$key', 1, " . time() . ") 
                  ON DUPLICATE KEY UPDATE attempts = attempts + 1, last_attempt = " . time());
    http_response_code(401);
    echo 'Invalid credentials';
}
?>

05Prevention Checklist

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.

07Related Recent Vulnerabilities