Weakness reference
CWE-640

Weak Password Recovery Mechanism for Forgotten Password

Password recovery mechanisms are often the weakest link in account security. When a site uses easily guessable security questions, sends recovery codes via…

01Summary

Password recovery mechanisms are often the weakest link in account security. When a site uses easily guessable security questions, sends recovery codes via insecure channels, or fails to rate-limit recovery attempts, attackers can bypass the primary password entirely and take over accounts. This weakness is particularly dangerous because users often trust recovery flows less than login flows and may not notice unauthorized access until significant damage is done.

02How It Happens

Password recovery is meant to help legitimate users regain access when they forget their credentials. However, many implementations prioritize convenience over security. Common flaws include:

- Predictable security questions
("What is your mother's maiden name?" or "What city were you born in?") that can be researched or guessed, especially for public figures or employees. - Insufficient verification steps
that skip multi-factor confirmation or rely on a single weak factor. - No rate limiting
on recovery attempts, allowing attackers to brute-force answers or codes. - Recovery codes sent via unencrypted email or SMS
without expiration, or stored in plaintext. - Sequential or predictable tokens
in recovery links that can be enumerated.

The attacker doesn't need to crack the password itself—they only need to answer the recovery questions or intercept the recovery token.

03Real-World Impact

Successful exploitation of a weak password recovery mechanism grants full account access without triggering password-change alerts or requiring knowledge of the original password. An attacker can change the email address, phone number, and security questions to lock out the legitimate owner. For high-value accounts (email, banking, social media, admin panels), this leads to identity theft, financial fraud, data exfiltration, or lateral movement into connected systems.

04Vulnerable & Fixed Patterns

Vulnerable pattern
def recover_password(username):
    user = db.query("SELECT * FROM users WHERE username = ?", (username,))
    if not user:
        return "User not found"
    
    # Ask a single, predictable security question
    question = "What is your mother's maiden name?"
    answer = request.form.get("answer")
    
    if answer.lower() == user['security_answer'].lower():
        # Generate recovery token with no expiration
        token = str(user['id']) + str(time.time())
        db.execute("UPDATE users SET recovery_token = ? WHERE id = ?", 
                   (token, user['id']))
        return f"Recovery link: /reset?token={token}"
    else:
        return "Incorrect answer"

Why it's vulnerable:
The security question is publicly guessable, the token is predictable (based on user ID and timestamp), there is no rate limiting on answer attempts, and the token has no expiration time.

Fixed pattern
import secrets
from datetime import datetime, timedelta

def recover_password(username):
    user = db.query("SELECT * FROM users WHERE username = ?", (username,))
    if not user:
        return "If that account exists, a recovery email has been sent"
    
    # Rate limit: check recent attempts
    recent_attempts = db.query(
        "SELECT COUNT(*) FROM recovery_attempts WHERE user_id = ? AND created_at > ?",
        (user['id'], datetime.now() - timedelta(hours=1))
    )
    if recent_attempts[0][0] > 3:
        return "Too many recovery attempts. Try again in 1 hour."
    
    # Log the attempt
    db.execute("INSERT INTO recovery_attempts (user_id, created_at) VALUES (?, ?)",
               (user['id'], datetime.now()))
    
    # Generate a cryptographically secure token with expiration
    token = secrets.token_urlsafe(32)
    expiry = datetime.now() + timedelta(minutes=15)
    db.execute("UPDATE users SET recovery_token = ?, recovery_expiry = ? WHERE id = ?",
               (token, expiry, user['id']))
    
    # Send via secure channel (email with HTTPS link)
    send_recovery_email(user['email'], token)
    return "Recovery email sent (generic response for all users)"
Vulnerable pattern
<?php
function recover_password($username) {
    global $wpdb;
    $user = $wpdb->get_row("SELECT * FROM users WHERE username = '$username'");
    
    if (!$user) {
        return "User not found";
    }
    
    // Single, guessable security question
    $question = "What is your pet's name?";
    $answer = $_POST['answer'];
    
    if (strtolower($answer) === strtolower($user->security_answer)) {
        // Token is just the user ID
        $token = $user->ID;
        $wpdb->update('users', 
            array('recovery_token' => $token),
            array('ID' => $user->ID)
        );
        return "Reset link: /reset.php?token=$token";
    }
    return "Incorrect answer";
}
?>

Why it's vulnerable:
The question is easily guessable, the token is just the user ID (trivially enumerable), there is no rate limiting, and no expiration is set on the token.

Fixed pattern
<?php
function recover_password($username) {
    global $wpdb;
    $user = $wpdb->get_user_by('login', $username);
    
    if (!$user) {
        // Generic response to prevent user enumeration
        wp_die("If that account exists, a recovery email has been sent.");
    }
    
    // Rate limit: check recent attempts
    $recent = $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM recovery_attempts WHERE user_id = %d AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)",
        $user->ID
    ));
    
    if ($recent > 3) {
        wp_die("Too many recovery attempts. Try again in 1 hour.");
    }
    
    // Log the attempt
    $wpdb->insert('recovery_attempts', array(
        'user_id' => $user->ID,
        'created_at' => current_time('mysql')
    ));
    
    // Generate secure token with expiration
    $token = bin2hex(random_bytes(32));
    $expiry = date('Y-m-d H:i:s', time() + 900); // 15 minutes
    
    $wpdb->update('users',
        array('recovery_token' => $token, 'recovery_expiry' => $expiry),
        array('ID' => $user->ID)
    );
    
    // Send via email (HTTPS link only)
    $reset_link = home_url("/reset/?token=" . urlencode($token));
    wp_mail($user->user_email, "Password Recovery", 
            "Click here to reset: " . esc_url($reset_link));
    
    return "Recovery email sent (generic response).";
}
?>

05Prevention Checklist

Use multi-factor recovery:
Require at least two independent verification methods (e.g., email + SMS, or security questions + email confirmation), never a single factor.
Avoid predictable security questions:
If you must use them, allow users to set custom questions or use a large pool of non-biographical questions. Better: skip them entirely in favor of email/SMS/authenticator-based recovery.
Generate cryptographically secure tokens:
Use secrets.token_urlsafe() (Python) or random_bytes() (PHP), never sequential or timestamp-based tokens.
Set short expiration times:
Recovery tokens should expire in 15–30 minutes. Expired tokens should be deleted, not just marked invalid.
Implement rate limiting:
Limit recovery attempts per user (e.g., 3 per hour) and per IP address to prevent brute-force attacks on answers or tokens.
Send recovery links via secure, authenticated channels only:
Use HTTPS email links or in-app notifications; never SMS or unencrypted email for sensitive resets.
Log all recovery attempts:
Track who requested recovery, when, and from which IP, and alert users of suspicious activity.
Avoid user enumeration:
Return the same generic message ("If that account exists, an email has been sent") regardless of whether the username was found.

06Signs You May Already Be Affected

- Unexpected password reset emails or notifications you did not request. - Recovery tokens or codes visible in plaintext in logs, emails, or database backups. - Users reporting account takeovers shortly after password resets, or recovery emails being intercepted. - No rate limiting on recovery endpoints, allowing rapid-fire requests in access logs.

07Related Recent Vulnerabilities