A client recently asked me about a common authentication implementation: storing client-generated password hash values directly in the database. While this approach might seem secure at first glance, it introduces several critical security vulnerabilities that could compromise user data.
The Problem
The question was straightforward: "Is it secure to store client-generated password hash values directly in the database? If not, how should we fix it?"
This scenario is more common than you might think. Developers often implement client-side password hashing thinking it adds an extra layer of security, but storing these hashes directly in the database creates new attack vectors.
Investigation
Let's examine the security issues with this approach:
1. Weak Client-Side Hashing
Client-side hashing is often implemented using weak algorithms:
- Weak hash algorithms: If the client uses MD5 or SHA-1, the hashes are vulnerable to brute force attacks and rainbow table lookups
- No salt: Without additional random salt values, attackers can use precomputed hash tables to crack passwords
2. Loss of Server-Side Control
When password hashing is delegated to the client:
- Cannot enforce security policies: The server cannot control hash algorithm strength or upgrade to more secure algorithms like bcrypt or Argon2
- Cannot implement unified password policies: Server cannot enforce password complexity requirements or additional security measures
3. Transmission Vulnerabilities
Client-side hashing creates potential attack vectors:
- Man-in-the-middle attacks: If hashing occurs over unencrypted connections, attackers can intercept original passwords
- Hash replay attacks: Attackers could potentially use captured hash values for authentication
Root Cause
The fundamental issue is misplaced trust. Security-critical operations like password hashing should be controlled by the server, not delegated to potentially compromised clients. Client-side environments are inherently less secure and harder to control than server environments.
Solution
Here are three approaches to fix this security issue, ranked by effectiveness:
Approach 1: Server-Side Password Hashing (Recommended)
This is the gold standard for password security:
- Client sends plaintext password over HTTPS
- Server handles all password hashing
from bcrypt import hashpw, gensalt, checkpw
def hash_password(password):
"""Generate secure hash with automatic salt handling"""
salt = gensalt()
hashed = hashpw(password.encode('utf-8'), salt)
return hashed
def verify_password(password, stored_hash):
"""Verify password against stored hash"""
return checkpw(password.encode('utf-8'), stored_hash)
# Registration
password = "user_input_password"
secure_hash = hash_password(password)
# Store secure_hash in database
# Login verification
is_valid = verify_password(input_password, stored_hash)Approach 2: Enhanced Client-Side Hashing (If Required)
If client-side hashing is absolutely necessary:
- Use HMAC-SHA256 with server-provided dynamic salt
- Server performs additional hashing on received values
// Client-side: Enhanced hashing with server salt
async function hashPasswordWithSalt(password, serverSalt) {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(password),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign(
"HMAC",
key,
encoder.encode(serverSalt)
);
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}# Server-side: Additional hashing of client result
def process_client_hash(client_hash):
"""Apply server-side bcrypt to client-generated hash"""
return hashpw(client_hash.encode('utf-8'), gensalt())Approach 3: Challenge-Response Authentication
For maximum security without transmitting passwords:
// Client-side: Challenge-response implementation
async function createChallengeResponse(password, challenge) {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(password),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
return crypto.subtle.sign(
"HMAC",
key,
encoder.encode(challenge)
);
}Security Impact Analysis
Let me address the follow-up question about salt security. If Approach 2 lacks proper salting, the security degradation is significant:
| Security Aspect | No Salt Impact | Risk Level |
|---|---|---|
| Rainbow table attacks | Highly vulnerable | Critical |
| Brute force resistance | Minimal protection | High |
| Password uniqueness | Same passwords = same hashes | High |
| Future upgrades | Difficult migration | Medium |
Without salt, modern GPUs can attempt billions of SHA-256 calculations per second, making password cracking feasible within hours or days.
Lessons Learned
Key Takeaways:
- Server-side control is crucial: Password security should never depend on client-side implementations
- Use proven algorithms: bcrypt, Argon2, or PBKDF2 are designed specifically for password hashing
- HTTPS is mandatory: Never transmit passwords over unencrypted connections
- Unique salts matter: Each password should have its own random salt to prevent rainbow table attacks
Prevention Tips:
- Always validate security assumptions during code review
- Use established cryptographic libraries instead of rolling your own
- Implement proper logging to detect authentication anomalies
- Regular security audits of authentication systems
- Consider modern alternatives like WebAuthn for enhanced security
Migration Strategy:
If you're currently storing client-generated hashes, plan a gradual migration:
- Implement server-side hashing for new users
- Migrate existing users during their next password change
- Maintain backward compatibility during the transition period
The bottom line: password security is too critical to leave to client-side implementations. Move password hashing to the server where you can control the security policies and use battle-tested cryptographic libraries.
