Host Header Manipulation Attacks

This demo demonstrates how attackers can abuse password reset functionality by manipulating HTTP Host headers to redirect reset links to malicious sites.

HTTP Request (Simplified):

POST /forgot-password HTTP/1.1 Host: secure-bank.com Content-Type: application/x-www-form-urlencoded email=victim@example.com

Email Preview:

How Host Header Attacks Work:

1
Problem: Applications often use the HTTP Host header to construct password reset links.
2
Attack: The attacker sends a password reset request for a victim's email but modifies the Host header to their malicious domain.
3
Vulnerability: The application generates a reset link using the attacker's domain but sends it to the victim's email.
4
Result: If the victim clicks the link, they're directed to the attacker's site which steals the reset token.
5
Exploitation: The attacker uses the stolen token on the legitimate site to reset the victim's password.

Vulnerable Code Example:


// 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}");
                    

Preventing Host Header Attacks:

  1. Hardcode Domain Names - Never use the Host header to generate URLs
  2. Validate Host Headers - Check against a whitelist of allowed domains
  3. Use Absolute URLs - Configure full URLs in application settings
  4. Set Domain Cookies - Use cookies bound to the real domain to prevent cross-domain attacks

// 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}";
}
                    

Comprehensive Password Reset Security

1. Secure Token Generation and Storage

// 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
}
        

2. Implement Proper Token Expiration and Rate Limiting

// 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);
        

3. Secure the Password Reset Process Flow

  1. Multi-Step Verification - Require additional verification for high-value accounts
  2. Notification System - Notify users of password changes through multiple channels
  3. Password Requirements - Enforce strong password policies during reset
  4. Audit Logging - Log all password reset attempts with relevant metadata
// 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' };
}
        

4. Additional Security Considerations

Reset token generated for ${user.email}

Token: ${token}

This token will never expire.

`; vulnerabilityHTML = `

Vulnerability: Tokens Without Expiration

Tokens that never expire remain valid indefinitely, creating a permanent security risk.

If the token is ever leaked or stolen, an attacker can use it at any time in the future.

This is particularly dangerous for users who may not realize their token has been compromised.

`; resetHistory.unshift(`Token generated for ${user.email} (no expiry) at ${new Date().toLocaleTimeString()}`); break; case 'reuse': // Token that can be reused multiple times user.resetToken = token; user.tokenCreated = now; user.tokenExpires = now + (60 * 60 * 1000); // 1 hour expiry user.tokenUsed = false; // Will not be marked as used when used responseHTML = `
Reset token generated for ${user.email}

Token: ${token}

This token will expire in 1 hour but will not be invalidated after use.

`; vulnerabilityHTML = `

Vulnerability: Reusable Tokens

Tokens that aren't invalidated after use can be used multiple times, even after a successful password reset.

This allows an attacker who intercepts or steals a token to use it even if the legitimate user has already used it.

For example, if a token is leaked through browser history or proxy logs, it remains valid until expiration.

`; resetHistory.unshift(`Token generated for ${user.email} (reusable) at ${new Date().toLocaleTimeString()}`); break; case 'unlimited': // Allow unlimited token generation user.resetToken = token; user.tokenCreated = now; user.tokenExpires = now + (60 * 60 * 1000); // 1 hour expiry user.tokenUsed = false; // Count how many tokens have been generated const tokenCount = resetHistory.filter(entry => entry.includes(`Token generated for ${user.email}`) && entry.includes(new Date().toLocaleDateString()) ).length + 1; responseHTML = `
Reset token generated for ${user.email}

Token: ${token}

This is token #${tokenCount} generated today for this user.

No rate limiting is enforced.

`; vulnerabilityHTML = `

Vulnerability: No Rate Limiting

Allowing unlimited token generation enables several attacks:

This can also be used for harassment or to distract users from noticing legitimate security emails.

`; resetHistory.unshift(`Token generated for ${user.email} (unlimited, #${tokenCount}) at ${new Date().toLocaleTimeString()}`); break; case 'secure': // Secure token handling // Check if there's a rate limit const recentTokens = resetHistory.filter(entry => entry.includes(`Token generated for ${user.email}`) && entry.includes(new Date().toLocaleDateString()) ); if (recentTokens.length >= 3) { responseHTML = `
Rate limit exceeded for ${user.email}

Too many reset requests. Maximum 3 tokens per day allowed.

Try again tomorrow or contact support.

`; resetHistory.unshift(`Token generation BLOCKED for ${user.email} (rate limit) at ${new Date().toLocaleTimeString()}`); } else { // Invalidate any existing tokens if (user.resetToken) { resetHistory.unshift(`Previous token for ${user.email} invalidated at ${new Date().toLocaleTimeString()}`); } user.resetToken = token; user.tokenCreated = now; user.tokenExpires = now + (15 * 60 * 1000); // 15 minutes expiry user.tokenUsed = false; responseHTML = `
Reset token generated for ${user.email}

Token: ${token}

This token will expire in 15 minutes and will be invalidated after use.

All previous tokens have been invalidated.

`; resetHistory.unshift(`Token generated for ${user.email} (secure) at ${new Date().toLocaleTimeString()}`); } vulnerabilityHTML = `

Secure Token Management

This implementation follows security best practices:

These measures significantly reduce the risk window and limit potential attacks.

`; break; } // Update the UI responseDiv.innerHTML = responseHTML; vulnerabilityDiv.innerHTML = vulnerabilityHTML; // Update the database table and history updateUserTable(); updateResetHistory(); } function useResetToken() { const userId = parseInt(document.getElementById('handlingUser').value); const issueType = document.getElementById('tokenIssue').value; const responseDiv = document.getElementById('handlingResponse'); // Find the user const user = userDatabase.find(u => u.id === userId); // Check if the user has a token if (!user.resetToken) { responseDiv.innerHTML = `
No reset token found

No token has been generated for this user yet.

`; return; } // Check if the token has expired (except for no-expiry tokens) const now = Date.now(); if (user.tokenExpires !== null && now > user.tokenExpires) { responseDiv.innerHTML = `
Token expired

The reset token has expired.

It expired at ${new Date(user.tokenExpires).toLocaleTimeString()}

`; resetHistory.unshift(`Token usage FAILED for ${user.email} (expired) at ${new Date().toLocaleTimeString()}`); updateResetHistory(); return; } // Check if the token has been used (for non-reusable tokens) if (user.tokenUsed === true && issueType !== 'reuse') { responseDiv.innerHTML = `
Token already used

This reset token has already been used and cannot be reused.

`; resetHistory.unshift(`Token usage FAILED for ${user.email} (already used) at ${new Date().toLocaleTimeString()}`); updateResetHistory(); return; } // Process the token use let responseHTML = ''; switch(issueType) { case 'no-expiry': // Token with no expiration - mark as used but it doesn't expire user.tokenUsed = true; responseHTML = `
Password successfully reset for ${user.email}

The token was accepted and remains valid for future use until manually invalidated.

`; resetHistory.unshift(`Password reset for ${user.email} (token remains valid) at ${new Date().toLocaleTimeString()}`); break; case 'reuse': // Token that can be reused - don't mark as used responseHTML = `
Password successfully reset for ${user.email}

The token was accepted and can be used again for future password resets.

It will remain valid until ${new Date(user.tokenExpires).toLocaleTimeString()}

`; resetHistory.unshift(`Password reset for ${user.email} (token reusable) at ${new Date().toLocaleTimeString()}`); break; case 'unlimited': // Normal token that is invalidated after use user.tokenUsed = true; responseHTML = `
Password successfully reset for ${user.email}

The token has been invalidated after use.

A new token can be generated immediately.

`; resetHistory.unshift(`Password reset for ${user.email} (token invalidated) at ${new Date().toLocaleTimeString()}`); break; case 'secure': // Secure token handling - invalidate after use and require waiting period user.tokenUsed = true; responseHTML = ` Password Reset Flaws Demo ← Back to All Vulnerabilities

Password Reset Vulnerabilities

Password reset functionalities are common in web applications but often contain vulnerabilities that can be exploited to gain unauthorized access to user accounts. This demo shows common flaws in password reset implementations and how to secure them.

Understanding Password Reset Vulnerabilities

1. Common Password Reset Flows

User requests
password reset
Application sends
reset email/SMS
User clicks link
with reset token
User sets
new password

2. Types of Password Reset Vulnerabilities

3. Common Attack Vectors

Password Reset Vulnerability Demonstrations

Weak Reset Token Generation

This demo shows the vulnerability of using predictable or easy-to-guess password reset tokens.

Generated Token:

No token generated yet.

Email Preview:

Token Generation History:

Vulnerability Analysis:

Select a token generation method to see its vulnerability analysis.

How to Implement Secure Tokens:


// Secure token generation (PHP example)
function generateSecureToken($userId) {
    // Generate a cryptographically secure random string
    $randomBytes = random_bytes(32);
    $token = bin2hex($randomBytes);
    
    // Store in database with expiration
    storeTokenInDatabase($userId, $token, time() + 3600); // 1 hour expiry
    
    return $token;
}

// Verify token securely
function verifyToken($userId, $token) {
    $storedToken = getTokenFromDatabase($userId);
    
    if (!$storedToken) {
        return false;
    }
    
    // Check if token has expired
    if (time() > $storedToken['expires_at']) {
        removeTokenFromDatabase($userId);
        return false;
    }
    
    // Use constant-time comparison to prevent timing attacks
    if (hash_equals($storedToken['token'], $token)) {
        // Token is valid
        return true;
    }
    
    return false;
}
                    

Token Handling Vulnerabilities

This demo illustrates various issues with how password reset tokens are managed after generation.

User Database (Simulation):

User ID Email Reset Token Token Created Token Expires Token Used
1 admin@example.com None N/A N/A N/A
2 user@example.com None N/A N/A N/A

System Response:

Reset Action History:

Vulnerability Analysis:

Select a token handling issue to see its vulnerability analysis.

Best Practices for Token Management:

  1. Set Short Expiration Times - Tokens should expire after 15-60 minutes
  2. One-Time Use - Invalidate tokens immediately after use
  3. Rate Limiting - Limit token generation requests (e.g., 3 per hour)
  4. Token Database - Store token creation time, expiry, and usage status
  5. Database Storage - Store tokens securely hashed, not in plaintext

// Proper token management example (Node.js)
async function handlePasswordReset(userId, token, newPassword) {
    // Get the token record from the database
    const tokenRecord = await db.tokens.findOne({
        userId,
        token: hashToken(token), // Store and compare hashed tokens
        used: false,
        expires: { $gt: new Date() }
    });
    
    if (!tokenRecord) {
        return { success: false, message: 'Invalid or expired token' };
    }
    
    try {
        // Begin a database transaction
        await db.beginTransaction();
        
        // Mark the token as used
        await db.tokens.update(
            { _id: tokenRecord._id },
            { $set: { used: true, usedAt: new Date() } }
        );
        
        // Update the user's password
        const hashedPassword = await bcrypt.hash(newPassword, 10);
        await db.users.update(
            { _id: userId },
            { 
                $set: { 
                    password: hashedPassword,
                    passwordChangedAt: new Date()
                }
            }
        );
        
        // Invalidate all other tokens for this user
        await db.tokens.updateMany(
            { userId, _id: { $ne: tokenRecord._id } },
            { $set: { used: true, invalidatedAt: new Date() } }
        );
        
        // Commit the transaction
        await db.commitTransaction();
        
        return { success: true };
    } catch (error) {
        // Rollback in case of error
        await db.rollbackTransaction();
        throw error;
    }
}
                    

User Enumeration Through Password Reset

This demo shows how password reset functionality can leak information about whether an email is registered in the system.

User Enumeration Vulnerability:

Password reset forms often inadvertently reveal whether an email exists in the database through different:

  • Response Messages - "Email not found" vs "Check your inbox"
  • HTTP Status Codes - 200 for success vs 404 for invalid email
  • Response Timing - Valid emails may take longer to process
  • Visual Differences - Different UI elements for valid/invalid emails

This information can be used by attackers for:

  • Creating lists of valid users for brute force attacks
  • Targeting specific high-value accounts
  • Combining with other vulnerabilities to exploit known users

Preventing User Enumeration:

  1. Consistent Messages - Return the same message regardless of email existence
  2. Consistent Timing - Add artificial delays to ensure consistent response times
  3. Consistent Flow - Follow the same process flow for both valid and invalid emails

// Secure implementation (PHP example)
function handlePasswordResetRequest($email) {
    // Always return the same message regardless of email existence
    $message = "If an account with that email exists, we have sent a password reset link.";
    
    // Check if the email exists in the background
    $user = findUserByEmail($email);
    
    if ($user) {
        // Generate token and send email in the background
        $token = generateSecureToken($user->id);
        sendPasswordResetEmail($email, $token);
    } else {
        // Simulate work to prevent timing attacks
        // This creates consistent response times
        usleep(random_int(100000, 300000)); // 100-300ms delay
    }
    
    // The user always gets the same response
    return $message;
}