Weak Password Recovery Mechanism for Forgotten Password
Description
Weak Password Recovery Mechanism for Forgotten Password occurs when an application implements a password recovery process that is susceptible to abuse or exploitation. Common weaknesses include: using easily guessable security questions, sending passwords in cleartext via email, using predictable password reset tokens, not expiring reset links, allowing unlimited reset attempts, revealing whether accounts exist, and using insecure secondary authentication methods. These weaknesses allow attackers to take over user accounts without knowing the original password.
Risk
Password recovery is a critical authentication bypass mechanism and a primary target for attackers. CVE-2023-7028 in GitLab CE/EE allowed password reset emails to be delivered to unverified email addresses, enabling complete account takeover without any user interaction and affecting versions 16.1 through 16.7. High-severity vulnerabilities have been found in HPE Cloudline, Ruijie Reyee OS, GLPI, IBM Security SOAR, and Dell PowerProtect. Weak recovery mechanisms enable mass account compromise, especially when combined with email list leaks or username enumeration. Attackers can take over administrative accounts, access sensitive data, and maintain persistent access.
Solution
Generate cryptographically random, single-use password reset tokens with short expiration times (15-60 minutes). Never reveal whether an account exists through different response messages. Use side-channel-resistant comparison for tokens. Require additional verification (SMS code, security key) for password resets. Implement rate limiting on reset requests. Invalidate all existing sessions after password change. Send password reset links, never actual passwords. Consider using passwordless authentication or passkeys. Log all password reset attempts for security monitoring.
Common Consequences
| Impact | Details |
|---|---|
| Authentication | Scope: Account Takeover Attackers gain full access to victim accounts by exploiting weak recovery mechanisms. |
| Confidentiality | Scope: Data Breach Compromised accounts provide access to all user data and potentially administrative functions. |
| Integrity | Scope: Unauthorized Actions Attackers can modify account settings, data, and perform actions as the victim. |
Example Code + Solution Code
Vulnerable Code
# VULNERABLE: Predictable reset tokens
import time
import hashlib
def generate_reset_token(email):
# Token based on predictable values - can be guessed!
token = hashlib.md5(f"{email}{time.time()}".encode()).hexdigest()
return token
# VULNERABLE: Revealing account existence
@app.route('/forgot-password', methods=['POST'])
def forgot_password():
email = request.form['email']
user = User.query.filter_by(email=email).first()
if user:
send_reset_email(user)
return "Reset email sent" # Different response reveals account exists
else:
return "Email not found" # Attacker knows email doesn't exist
# VULNERABLE: No token expiration
def verify_reset_token(token):
reset = PasswordReset.query.filter_by(token=token).first()
if reset:
return reset.user # Token never expires!
return None
// VULNERABLE: Security questions with guessable answers
public class PasswordRecovery {
private static final String[] SECURITY_QUESTIONS = {
"What is your mother's maiden name?", // Often public info
"What is your pet's name?", // Easy to find on social media
"What city were you born in?" // Public records
};
public boolean verifySecurityAnswer(User user, String answer) {
// Case-insensitive comparison reveals partial information
return user.getSecurityAnswer().equalsIgnoreCase(answer);
}
// VULNERABLE: Sending password in cleartext email
public void resetPassword(User user) {
String newPassword = generateSimplePassword(); // Weak password
user.setPassword(newPassword); // Stored without hashing!
// Sending actual password in email - interceptable!
emailService.send(user.getEmail(),
"Your new password is: " + newPassword);
}
}
// VULNERABLE: Reset token in URL without expiration
app.get('/reset-password/:token', (req, res) => {
const token = req.params.token;
// Token stored permanently, never expires
const reset = db.findResetToken(token);
if (reset) {
// Token reusable - attacker can reset multiple times
res.render('reset-form', { userId: reset.userId });
}
});
// VULNERABLE: No rate limiting
app.post('/forgot-password', (req, res) => {
// Attacker can request unlimited resets
// to spam victim or enumerate accounts
const user = db.findByEmail(req.body.email);
if (user) {
sendResetEmail(user);
}
res.json({ message: 'If account exists, email was sent' });
});
Fixed Code
# SAFE: Cryptographically secure reset tokens
import secrets
from datetime import datetime, timedelta
def generate_reset_token_safe():
# 256 bits of cryptographic randomness
return secrets.token_urlsafe(32)
# SAFE: Consistent response regardless of account existence
@app.route('/forgot-password', methods=['POST'])
@rate_limit(max_requests=3, per_minutes=15)
def forgot_password_safe():
email = request.form['email']
user = User.query.filter_by(email=email).first()
if user:
# Generate secure token with expiration
token = generate_reset_token_safe()
expiration = datetime.utcnow() + timedelta(minutes=30)
# Invalidate any existing tokens
PasswordReset.query.filter_by(user_id=user.id).delete()
# Store hashed token
token_hash = hash_token(token)
reset = PasswordReset(
user_id=user.id,
token_hash=token_hash,
expires_at=expiration
)
db.session.add(reset)
db.session.commit()
send_reset_email(user, token)
# Same response regardless of whether user exists
return jsonify({
'message': 'If an account exists with this email, a reset link has been sent'
})
# SAFE: Token verification with expiration and single-use
def verify_reset_token_safe(token):
token_hash = hash_token(token)
reset = PasswordReset.query.filter_by(token_hash=token_hash).first()
if not reset:
return None
# Check expiration
if datetime.utcnow() > reset.expires_at:
db.session.delete(reset)
db.session.commit()
return None
return reset.user
def complete_reset(token, new_password):
user = verify_reset_token_safe(token)
if not user:
raise InvalidTokenError()
# Update password with proper hashing
user.password_hash = hash_password(new_password)
# Invalidate the token (single-use)
PasswordReset.query.filter_by(user_id=user.id).delete()
# Invalidate all existing sessions
Session.query.filter_by(user_id=user.id).delete()
db.session.commit()
# Log the password change
audit_log.info(f"Password reset completed for user {user.id}")
// SAFE: Secure password recovery implementation
import java.security.SecureRandom;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
public class SecurePasswordRecovery {
private static final SecureRandom secureRandom = new SecureRandom();
private static final int TOKEN_LENGTH = 32;
private static final int TOKEN_EXPIRY_MINUTES = 30;
public String initiatePasswordReset(String email) {
User user = userRepository.findByEmail(email);
if (user != null) {
// Generate cryptographically secure token
byte[] tokenBytes = new byte[TOKEN_LENGTH];
secureRandom.nextBytes(tokenBytes);
String token = Base64.getUrlEncoder().withoutPadding()
.encodeToString(tokenBytes);
// Store hashed token
String tokenHash = hashToken(token);
Instant expiry = Instant.now().plus(TOKEN_EXPIRY_MINUTES, ChronoUnit.MINUTES);
// Invalidate existing tokens
passwordResetRepository.deleteByUserId(user.getId());
PasswordResetToken resetToken = new PasswordResetToken(
user.getId(),
tokenHash,
expiry
);
passwordResetRepository.save(resetToken);
// Send reset link (not the password!)
emailService.sendPasswordResetLink(user.getEmail(), token);
}
// Same response regardless of user existence
return "If an account exists, a reset link has been sent";
}
public boolean resetPassword(String token, String newPassword) {
String tokenHash = hashToken(token);
PasswordResetToken resetToken = passwordResetRepository
.findByTokenHash(tokenHash);
if (resetToken == null) {
auditLog.warn("Invalid reset token attempted");
return false;
}
// Check expiration
if (Instant.now().isAfter(resetToken.getExpiry())) {
passwordResetRepository.delete(resetToken);
return false;
}
// Update password with strong hashing
User user = userRepository.findById(resetToken.getUserId());
user.setPasswordHash(BCrypt.hashpw(newPassword, BCrypt.gensalt(12)));
userRepository.save(user);
// Invalidate token (single-use)
passwordResetRepository.delete(resetToken);
// Invalidate all sessions
sessionRepository.deleteByUserId(user.getId());
// Send confirmation (not the password!)
emailService.sendPasswordChangeConfirmation(user.getEmail());
auditLog.info("Password reset completed for user: " + user.getId());
return true;
}
private String hashToken(String token) {
return Hashing.sha256().hashString(token, StandardCharsets.UTF_8).toString();
}
}
// SAFE: Complete secure password reset flow
const crypto = require('crypto');
const argon2 = require('argon2');
class SecurePasswordResetService {
constructor(db, emailService, rateLimiter) {
this.db = db;
this.emailService = emailService;
this.rateLimiter = rateLimiter;
}
async initiateReset(email, ip) {
// Rate limit by IP
if (!await this.rateLimiter.check(ip, 'reset', 3, 900)) {
throw new RateLimitError('Too many reset requests');
}
const user = await this.db.findUserByEmail(email);
if (user) {
// Generate secure token
const token = crypto.randomBytes(32).toString('base64url');
const tokenHash = crypto.createHash('sha256')
.update(token).digest('hex');
const expiresAt = new Date(Date.now() + 30 * 60 * 1000); // 30 minutes
// Invalidate existing tokens
await this.db.deleteResetTokens(user.id);
// Store hashed token
await this.db.createResetToken({
userId: user.id,
tokenHash,
expiresAt,
ipAddress: ip
});
// Send reset link
await this.emailService.sendResetLink(user.email, token);
}
// Always return same response
return { message: 'If account exists, reset link was sent' };
}
async completeReset(token, newPassword, ip) {
const tokenHash = crypto.createHash('sha256')
.update(token).digest('hex');
const resetToken = await this.db.findResetToken(tokenHash);
if (!resetToken) {
await this.logFailedAttempt(ip);
throw new InvalidTokenError();
}
if (new Date() > resetToken.expiresAt) {
await this.db.deleteResetToken(resetToken.id);
throw new ExpiredTokenError();
}
// Hash new password with Argon2
const passwordHash = await argon2.hash(newPassword, {
type: argon2.argon2id,
memoryCost: 65536,
timeCost: 3,
parallelism: 4
});
// Update password
await this.db.updateUserPassword(resetToken.userId, passwordHash);
// Invalidate token (single-use)
await this.db.deleteResetToken(resetToken.id);
// Invalidate all sessions
await this.db.deleteUserSessions(resetToken.userId);
// Send confirmation
const user = await this.db.findUserById(resetToken.userId);
await this.emailService.sendPasswordChangedNotification(user.email);
return { message: 'Password successfully reset' };
}
}
Exploited in the Wild
GitLab Account Takeover (GitLab, 2024)
CVE-2023-7028 in GitLab CE/EE 16.1-16.7 allowed password reset emails to be sent to unverified email addresses, enabling complete account takeover without user interaction or authentication—remotely exploitable with repeatable success.
HPE Cloudline Server (HPE, 2025)
High-severity weak password recovery vulnerability in HPE Cloudline CL4150 Gen10 Server allowed attackers to bypass authentication through flawed recovery mechanisms.
Ruijie Reyee OS (Ruijie, 2024)
High-severity password recovery vulnerability in Ruijie Reyee OS network equipment allowed attackers to take over device management accounts.
Tools to test/exploit
-
Burp Suite — test password reset flows and token predictability.
-
OWASP ZAP — automated testing of authentication mechanisms.
-
Hydra — test rate limiting on reset endpoints.
CVE Examples
-
CVE-2023-7028 — GitLab password reset to unverified email.
-
CVE-2017-12426 — GitLab password reset token disclosure.
-
CVE-2012-3427 — Red Hat JBoss password reset vulnerability.
References
-
MITRE. "CWE-640: Weak Password Recovery Mechanism for Forgotten Password." https://cwe.mitre.org/data/definitions/640.html
-
OWASP. "Forgot Password Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html