Safeguard
Application Security•

CWE-640 Weak Password Recovery for Forgotten Password

A weak reset flow undoes a strong login. Common failures, real WordPress plugin and token reuse cases, and how to harden password recovery.

Safeguard Security Team
5 min read

CWE-640, Weak Password Recovery Mechanism for Forgotten Password, covers recovery or reset flows that let someone other than the account owner get in. It affects any product that lets users recover a password without knowing the original, which is nearly every product with accounts.

MITRE makes a blunt point: weak password recovery schemes completely undermine a strong password authentication scheme. You can require 16 characters and hardware keys at login, and none of it matters if "forgot password" can be talked into resetting the account.

How it happens

The recovery flow is a second login path, and it often gets less care than the first. MITRE lists several ways it goes weak. The security question may be too easy to guess or find the answer to, for example because it is a common question. Input to the recovery form may not be filtered or validated. The number of guesses may not be limited. The user may be allowed to control the email address the new password is sent to.

The record's observed examples are recent and varied:

  • A WordPress plugin's password reset allowed a brute force attack on the one-time password (CVE-2025-1570). A second WordPress plugin had the same kind of issue.
  • A password recovery mechanism for an AI developer toolkit did not invalidate the reset token after use, so attackers could reuse it to change the victim's password.
  • A web conference product reset passwords to random 8-digit values, allowing brute force attacks after retrieving the hash.

The theme is the same. A short code, with no limit on tries, is a password with a small keyspace and an open door.

# Vulnerable: 6-digit code, unlimited attempts, token never expires
def verify_reset(user, code):
    return user.reset_code == code

def reset_password(user, code, new_pw):
    if verify_reset(user, code):
        user.set_password(new_pw)       # token still valid afterwards
# Safer: long random token, expiry, attempt limit, single use
import secrets, hmac, time

def issue_token(user):
    token = secrets.token_urlsafe(32)
    user.reset_hash = sha256(token)
    user.reset_expires = time.time() + 900
    user.reset_attempts = 0
    return token                         # emailed to the address on file

def reset_password(user, token, new_pw):
    if user.reset_attempts >= 5 or time.time() > user.reset_expires:
        raise Denied()
    user.reset_attempts += 1
    if not hmac.compare_digest(user.reset_hash, sha256(token)):
        raise Denied()
    user.set_password(new_pw)
    user.reset_hash = None               # single use

The second example goes beyond the MITRE text in a few details, such as hashing the stored token and the 15 minute expiry. Those are common practice. The record itself calls for throttling, disabling after a small number of failures and never letting the user choose the destination address.

What an attacker gains

MITRE lists three outcomes:

  • Gaining privileges or assuming identity: unauthorized access by retrieving a legitimate user's credentials.
  • Availability: denial of service by running a brute force attack on the recovery mechanism with legitimate user IDs.
  • Integrity: the system's security functionality is turned against the system.

The second item is easy to forget. Recovery flows that lock accounts or send many emails can be abused to annoy or block users even when they cannot be broken.

How to find it

The record lists no detection methods, so use a review and test checklist:

  1. Read the flow end to end: request, delivery, verification, change.
  2. Check the strength of the secret. How many bits? Is it generated by a secure random source?
  3. Try guessing. Send many wrong codes or answers and see if the system slows, locks or keeps accepting.
  4. Reuse a token after a successful reset. It should fail.
  5. Try changing the destination email or phone in the request.
  6. Look for security questions with answers that are public or easy to guess.
  7. Compare responses for known and unknown accounts. Different messages reveal which accounts exist.

If you use third-party plugins or toolkits for authentication, check their version history. The record's examples were in plugins, which is a reminder that recovery code you did not write is still your exposure. A free in-browser dependency scan at gold.safeguard.sh/scan shows known issues in what you ship, with no login, the file stays on your machine, up to 100 dependencies.

How to fix it

MITRE's mitigations, in plain steps:

  1. Thoroughly filter and validate all input supplied to the recovery mechanism.
  2. Do not use standard weak security questions, and use several questions.
  3. Throttle incorrect answers. Disable recovery after a small number of incorrect guesses.
  4. Require that the user answers the security question correctly before the new password is sent to the email address of record.
  5. Never let the user control the email address the new password is sent to.

Practical additions: prefer long random single-use links over short codes, expire them quickly, invalidate them after use or when a newer one is issued, and notify the account owner when a reset happens.

Quick check

Why can a short one-time code be a problem?

If attempts are not limited, an attacker can try every value. The listed WordPress plugin cases allowed brute force of the one-time password.

What should happen to a reset token after use?

It should stop working. One listed case failed to invalidate the token, so attackers could reuse it.

Who chooses where the new password goes?

Never the user at reset time. It must go to the address of record.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.