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' };
}