Use of Password Hash Instead of Password for Authentication

Description

Use of Password Hash Instead of Password for Authentication is an authentication weakness where a system accepts password hashes directly from clients for authentication, comparing them against stored hashes rather than receiving the plaintext password and hashing it server-side. While this approach may seem to enhance security by avoiding password transmission and reducing server load, it actually creates a critical vulnerability. Attackers who obtain password hashes through any means (SQL injection, data breaches, memory dumps) can replay the stolen hashes directly to authenticate without ever knowing the original passwords.

Risk

This design fundamentally breaks the security model of password hashing. The entire purpose of storing password hashes is to prevent attackers who compromise the password database from immediately gaining access—they should need to crack the hashes first. With client-side hashing, the hash itself becomes the credential. Pass-the-hash attacks allow attackers to authenticate using captured hashes. SQL injection that extracts hashes provides immediate account access. Memory scraping on the server yields authenticatable credentials. Network interception of hashes (even over encrypted channels) enables replay attacks. The server-side hash comparison provides no additional security over storing plaintext passwords.

Solution

Always perform password hashing server-side. Transmit plaintext passwords from the client (over TLS/HTTPS) and hash them on the server for comparison. Use proper password hashing algorithms (bcrypt, Argon2, scrypt) with unique salts per password. If network transmission is a concern, use TLS/HTTPS—not client-side hashing. Consider additional authentication factors (MFA) for sensitive accounts. Implement monitoring for credential abuse patterns. Store only the hash; never store or transmit plaintext passwords. If zero-knowledge authentication is truly needed, use purpose-built protocols like SRP (Secure Remote Password).

Common Consequences

ImpactDetails
Access ControlScope: Access Control

Bypass Protection Mechanism - Attackers can authenticate using captured hashes without cracking them, bypassing the intended protection of password hashing.
Access ControlScope: Access Control

Gain Privileges or Assume Identity - Hash replay allows attackers to impersonate any user whose hash they possess.
ConfidentialityScope: Confidentiality

Read Application Data - Authenticated access enables attackers to access user data and system resources.

Example Code

Vulnerable Code

// Vulnerable: Client-side hashing
// Client code
async function login(username, password) {
    // Vulnerable: Password is hashed on client
    const passwordHash = await crypto.subtle.digest(
        'SHA-256',
        new TextEncoder().encode(password)
    );
    const hashHex = Array.from(new Uint8Array(passwordHash))
        .map(b => b.toString(16).padStart(2, '0'))
        .join('');

    // Hash is sent to server
    const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ username, passwordHash: hashHex })
    });
    return response.json();
}

// Server code
app.post('/api/login', (req, res) => {
    const { username, passwordHash } = req.body;

    // Vulnerable: Server just compares hashes
    const user = users.findByUsername(username);
    if (user && user.storedHash === passwordHash) {
        // Attacker with stolen hash can authenticate directly!
        createSession(user);
        res.json({ success: true });
    } else {
        res.status(401).json({ error: 'Invalid credentials' });
    }
});
# Vulnerable: Hash-based authentication
import hashlib

class VulnerableAuthSystem:

    def authenticate(self, username, password_hash):
        """
        Vulnerable: Accepts hash directly from client
        """
        user = self.get_user(username)
        if user is None:
            return False

        # Vulnerable: Direct hash comparison
        # Attacker who steals hash can replay it
        if user['password_hash'] == password_hash:
            return True
        return False
// Vulnerable: Client sends pre-computed hash
public class VulnerableAuthService {

    public boolean authenticate(String username, String clientHash) {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            return false;
        }

        // Vulnerable: Comparing client-provided hash to stored hash
        // If attacker has the hash, they can log in directly
        return user.getPasswordHash().equals(clientHash);
    }
}
// Vulnerable: Hash comparison authentication
<?php
function vulnerable_login($username, $client_hash) {
    $user = get_user($username);
    if (!$user) {
        return false;
    }

    // Vulnerable: Hash is the credential
    if ($user['password_hash'] === $client_hash) {
        // SQL injection revealing hashes = immediate account compromise
        create_session($user);
        return true;
    }
    return false;
}
?>
// Vulnerable: Network protocol using hash authentication
int vulnerable_auth(connection_t *conn) {
    char username[64];
    char client_hash[65];  // SHA-256 hex

    recv(conn->socket, username, sizeof(username), 0);
    recv(conn->socket, client_hash, sizeof(client_hash), 0);

    user_t *user = lookup_user(username);
    if (!user) {
        return AUTH_FAILED;
    }

    // Vulnerable: Hash comparison - intercepted hash can be replayed
    if (strcmp(user->password_hash, client_hash) == 0) {
        return AUTH_SUCCESS;
    }

    return AUTH_FAILED;
}

Fixed Code

// Fixed: Server-side hashing
// Client code - sends plaintext over HTTPS
async function login(username, password) {
    // Fixed: Send plaintext password (over HTTPS)
    const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
    });
    return response.json();
}

// Server code - hashes server-side
const bcrypt = require('bcrypt');

app.post('/api/login', async (req, res) => {
    const { username, password } = req.body;

    const user = await users.findByUsername(username);
    if (!user) {
        // Constant time delay to prevent timing attacks
        await bcrypt.compare(password, '$2b$10$dummy.hash.for.timing');
        return res.status(401).json({ error: 'Invalid credentials' });
    }

    // Fixed: Server hashes and compares
    // Stolen hash cannot be used directly - attacker must crack it
    const valid = await bcrypt.compare(password, user.passwordHash);
    if (valid) {
        createSession(user);
        res.json({ success: true });
    } else {
        res.status(401).json({ error: 'Invalid credentials' });
    }
});
# Fixed: Server-side password hashing
import bcrypt

class FixedAuthSystem:

    def authenticate(self, username, password):
        """
        Fixed: Password is hashed server-side
        """
        user = self.get_user(username)
        if user is None:
            # Constant-time comparison to prevent timing attacks
            bcrypt.checkpw(b'dummy', b'$2b$12$dummy.hash.for.timing.attack.prevention')
            return False

        # Fixed: Server hashes the password and compares
        # Even if attacker steals hash, they must crack it first
        password_bytes = password.encode('utf-8')
        stored_hash = user['password_hash'].encode('utf-8')

        return bcrypt.checkpw(password_bytes, stored_hash)

    def set_password(self, user_id, password):
        """Store properly hashed password"""
        password_bytes = password.encode('utf-8')
        # Fixed: Use bcrypt with good cost factor
        hashed = bcrypt.hashpw(password_bytes, bcrypt.gensalt(rounds=12))
        self.update_user_hash(user_id, hashed.decode('utf-8'))
// Fixed: Server-side password verification
import org.mindrot.jbcrypt.BCrypt;

public class FixedAuthService {

    public boolean authenticate(String username, String password) {
        User user = userRepository.findByUsername(username);

        if (user == null) {
            // Prevent timing attacks
            BCrypt.checkpw(password, "$2a$12$dummy.hash.for.timing");
            return false;
        }

        // Fixed: Server verifies password against stored hash
        // Stolen hash requires cracking before use
        return BCrypt.checkpw(password, user.getPasswordHash());
    }

    public void setPassword(User user, String password) {
        // Fixed: Hash password server-side before storage
        String hash = BCrypt.hashpw(password, BCrypt.gensalt(12));
        user.setPasswordHash(hash);
        userRepository.save(user);
    }
}
// Fixed: Server-side password hashing with PHP
<?php
function fixed_login($username, $password) {
    $user = get_user($username);

    if (!$user) {
        // Timing attack prevention
        password_verify($password, '$2y$12$dummyhashfortimingattack');
        return false;
    }

    // Fixed: Use password_verify - server-side comparison
    if (password_verify($password, $user['password_hash'])) {
        create_session($user);
        return true;
    }
    return false;
}

function set_password($user_id, $password) {
    // Fixed: Hash password server-side
    $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
    update_user_password($user_id, $hash);
}
?>
// Fixed: Proper authentication with server-side hashing
import (
    "golang.org/x/crypto/bcrypt"
)

func authenticate(username, password string) bool {
    user, err := getUserByUsername(username)
    if err != nil || user == nil {
        // Timing attack prevention
        bcrypt.CompareHashAndPassword(
            []byte("$2a$12$dummyhashfortiming"),
            []byte(password))
        return false
    }

    // Fixed: Server compares password against stored hash
    err = bcrypt.CompareHashAndPassword(
        []byte(user.PasswordHash),
        []byte(password))

    return err == nil
}

func setPassword(userID int, password string) error {
    // Fixed: Hash server-side with appropriate cost
    hash, err := bcrypt.GenerateFromPassword(
        []byte(password),
        bcrypt.DefaultCost)
    if err != nil {
        return err
    }

    return updateUserPassword(userID, string(hash))
}

  • CWE-1390: Weak Authentication (parent)
  • CWE-602: Client-Side Enforcement of Server-Side Security (related)
  • CWE-287: Improper Authentication (related)
  • CWE-916: Use of Password Hash With Insufficient Computational Effort (related)

References

  1. MITRE Corporation. "CWE-836: Use of Password Hash Instead of Password for Authentication." https://cwe.mitre.org/data/definitions/836.html
  2. CAPEC-644. "Use of Captured Hashes (Pass The Hash)."
  3. OWASP. "Password Storage Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html