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
| Impact | Details |
|---|---|
| Access Control | Scope: Access Control Bypass Protection Mechanism - Attackers can authenticate using captured hashes without cracking them, bypassing the intended protection of password hashing. |
| Access Control | Scope: Access Control Gain Privileges or Assume Identity - Hash replay allows attackers to impersonate any user whose hash they possess. |
| Confidentiality | Scope: 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))
}
Related CWEs
- 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
- MITRE Corporation. "CWE-836: Use of Password Hash Instead of Password for Authentication." https://cwe.mitre.org/data/definitions/836.html
- CAPEC-644. "Use of Captured Hashes (Pass The Hash)."
- OWASP. "Password Storage Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html