This demo demonstrates how attackers can abuse password reset functionality by manipulating HTTP Host headers to redirect reset links to malicious sites.
Click the link below to reset your password:
// Vulnerable PHP code that uses Host header directly
function generateResetLink($token) {
// VULNERABLE: Using the Host header to build the reset URL
$host = $_SERVER['HTTP_HOST'];
return "https://{$host}/reset-password?token={$token}";
}
// Generating and sending the reset email
$token = generateResetToken($user->id);
$resetLink = generateResetLink($token);
sendEmail($user->email, "Password Reset", "Click here to reset: {$resetLink}");
// Secure PHP code with hardcoded domains
function generateResetLink($token) {
// Hardcoded domain name from configuration
$domain = config('app.url'); // e.g., "https://secure-bank.com"
// Create reset URL with configured domain
return "{$domain}/reset-password?token={$token}";
}
// Additional validation if using Host header (not recommended)
function validateHostHeader($host) {
$allowedHosts = ['secure-bank.com', 'www.secure-bank.com'];
if (!in_array($host, $allowedHosts)) {
// Log potential attack attempt
logSecurityEvent("Host header manipulation attempt: {$host}");
// Use default secure domain
return config('app.url');
}
return "https://{$host}";
}
// Generate cryptographically secure tokens const crypto = require('crypto'); function generateSecureToken() { // Create a 32-byte random token return crypto.randomBytes(32).toString('hex'); } // Store tokens securely async function createPasswordResetToken(userId) { const token = generateSecureToken(); const hashedToken = crypto.createHash('sha256').update(token).digest('hex'); // Store hash, not the actual token await db.tokens.insertOne({ userId, token: hashedToken, createdAt: new Date(), expiresAt: new Date(Date.now() + 3600000), // 1 hour used: false }); return token; // Return the actual token to be sent via email }
// Check token expiration async function validateToken(userId, token) { // Hash the provided token for comparison const hashedToken = crypto.createHash('sha256').update(token).digest('hex'); const tokenRecord = await db.tokens.findOne({ userId, token: hashedToken, used: false, expiresAt: { $gt: new Date() } }); return !!tokenRecord; } // Rate limiting implementation const rateLimit = require('express-rate-limit'); const resetLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 3, // 3 requests per hour message: 'Too many password reset requests, please try again later.' }); app.post('/forgot-password', resetLimiter, forgotPasswordHandler);
// Example of a complete password reset flow async function resetPassword(userId, token, newPassword) { // Validate the token if (!(await validateToken(userId, token))) { return { success: false, message: 'Invalid or expired token' }; } // Validate the new password strength if (!isStrongPassword(newPassword)) { return { success: false, message: 'Password does not meet security requirements' }; } // Change the password const hashedPassword = await bcrypt.hash(newPassword, 12); await db.users.updateOne( { _id: userId }, { $set: { password: hashedPassword, passwordChangedAt: new Date() } } ); // Invalidate the used token await db.tokens.updateOne( { userId, token: crypto.createHash('sha256').update(token).digest('hex') }, { $set: { used: true, usedAt: new Date() } } ); // Log the password change await logSecurityEvent({ userId, event: 'PASSWORD_RESET', ipAddress: requestIp, userAgent: requestUserAgent, timestamp: new Date() }); // Send notification to user await sendPasswordChangeNotification(userId); return { success: true, message: 'Password reset successful' }; }