CWE-760: Use of One-Way Hash Without Salt
Overview
Hashing passwords without unique salts enables rainbow table attacks and password cracking across multiple accounts. Attackers precompute hashes for common passwords and instantly crack unsalted hashes, compromising all accounts using the same password.
OWASP Classification
A04:2025 - Cryptographic Failures
Risk
Critical: Unsalted password hashes enable mass password cracking via rainbow tables, dictionary attacks, and hash databases, allowing attackers to compromise thousands of accounts instantly once password database is stolen.
Remediation Strategy
Use Password Hashing Functions (Primary Defense)
Use modern password hashing algorithms that automatically handle salting:
- bcrypt: Industry standard, automatic salt generation, configurable cost factor
- Argon2: Winner of Password Hashing Competition, best current choice
- scrypt: Memory-hard function, good alternative
- PBKDF2: Acceptable when using high iteration count (600,000+ per OWASP 2023)
Key characteristics:
- Automatically generate unique random salt for each password
- Computationally expensive (adjustable work factor)
- Store algorithm, cost parameters, salt, and hash together
- Designed specifically for password storage
Never Use Fast Hash Functions for Passwords
Avoid these for password hashing:
- MD5: Broken, extremely fast to crack
- SHA-1: Deprecated, too fast
- SHA-256/SHA-512: Too fast without key derivation
- Any unsalted hash: Vulnerable to rainbow tables
Generate Cryptographically Random Salts
If implementing salting manually:
- Use cryptographically secure random number generator
- Minimum 16 bytes (128 bits), prefer 32 bytes (256 bits)
- Generate unique salt for EACH password
- Never reuse salts across users
- Store salt alongside the hash
Use Key Derivation Functions (KDF)
If you must use SHA-256/SHA-512:
- Wrap in a KDF like PBKDF2
- Use high iteration count (100,000+ minimum)
- Combine password with unique random salt
- Store iterations, salt, and hash together
Remediation Steps
Core principle: Always salt password hashes; use strong password hashing and unique salts per credential.
Follow these steps to fix security findings for CWE-760:
- Identify current hashing method
- Choose appropriate password hashing function
- Implement password hashing
- Plan migration strategy
- Implement dual-read verification
- Test with sample passwords
- Monitor migration progress
Why Salts Are Critical
Without Salt:
User1: password123 → hash: 482c811da5d5b4bc6d...
User2: password123 → hash: 482c811da5d5b4bc6d...
User3: password123 → hash: 482c811da5d5b4bc6d...
Attacker: 1. Sees same hash for multiple users 2. Knows they have same password 3. Uses rainbow table: 482c811da5d5b4bc6d... = "password123" 4. Compromises ALL three accounts instantly
With Salt:
User1: password123 + salt1 → hash: a8f5f167f44f4964e6c998dee827110c
User2: password123 + salt2 → hash: 2d5ec38b0df6d8e94c3e7e8c8e7e8c8d
User3: password123 + salt3 → hash: 7f9c7ca8e8c7e8c7e8c7e8c7e8c7e8c7
Attacker: 1. All hashes are different 2. Must brute force EACH password individually 3. Rainbow tables useless 4. Takes years instead of seconds
Common Vulnerable Patterns
Static/Shared Salt
// All users share the same salt
STATIC_SALT = "mysecretkey"
password_hash = hash(password + STATIC_SALT)
// Attacker builds rainbow table with YOUR salt, cracks all passwords!
Why this is vulnerable: Using the same salt for all users means attackers only need to build one rainbow table with your static salt to crack every password in the database. The salt becomes a known constant that provides zero protection - identical passwords still produce identical hashes, and precomputation attacks work exactly as they would without any salt at all.
Predictable Salt
// Salt derived from known/predictable values
salt = user_id // Attacker knows user IDs
salt = timestamp // Predictable
salt = username // Known to attacker
password_hash = hash(password + salt)
Why this is vulnerable: Attackers can easily obtain or guess predictable salts (user IDs are in URLs, usernames are public, timestamps have limited ranges). They can precompute rainbow tables for all likely salt values or compute hashes on-the-fly since the salt space is small. For example, with user IDs 1-1,000,000, attackers precompute hashes for common passwords with all million possible salts, then instantly crack passwords across your entire user base.
Fast Hashing
// Single iteration of fast hash
hash = SHA256(password + salt)
// Even with salt, attacker tests billions per second
Why this is vulnerable: Modern GPUs compute SHA-256 at ~2 billion hashes per second, letting attackers brute-force an 8-character password (all lowercase) in under 6 seconds even with unique salts. Fast hash functions like MD5, SHA-1, and SHA-256 were designed for speed and data integrity, not password security. Without computational slowdown (key stretching via iterations or memory-hard operations), attackers leverage massive parallelization to crack weak-to-moderate passwords in minutes to hours instead of years.
No Salt
password_hash = MD5(password) // Extremely vulnerable!
store_in_database(user_id, password_hash)
// Same password for multiple users:
User1: "password123" → hash: 482c811da5d5b4bc6d497c3e7e1e92
User2: "password123" → hash: 482c811da5d5b4bc6d497c3e7e1e92
User3: "password123" → hash: 482c811da5d5b4bc6d497c3e7e1e92
// Attacker cracks ONE hash, compromises ALL three accounts!
Why this is vulnerable: Identical passwords produce identical hashes, letting attackers use precomputed rainbow tables to instantly reverse hashes for common passwords. The lack of salt reveals which users share passwords - cracking one hash compromises all accounts with that hash. MD5 processes 8+ billion hashes per second on GPUs, making even moderately complex passwords crackable in hours, and freely available rainbow tables cover passwords up to 14 characters.
Secure Patterns
Unique Salts
// User 1
salt1 = random_bytes(32)
hash1 = password_hash_function(password="password123", salt=salt1)
store(user1, salt1, hash1)
// User 2 (same password)
salt2 = random_bytes(32) // Different salt!
hash2 = password_hash_function(password="password123", salt=salt2)
store(user2, salt2, hash2)
// Result: hash1 ≠ hash2 even though passwords are identical
// Attacker must brute force EACH password individually
// Rainbow tables are useless
Why this works: Unique cryptographically random salts (minimum 16 bytes, preferably 32) generated for each password hash transform the password hashing problem from "crack many hashes at once" to "crack each hash individually". When User1 and User2 both use "password123", unique salts ensure their stored hashes are completely different, preventing attackers from recognizing password reuse and eliminating the effectiveness of precomputed rainbow tables (which would need to be regenerated for every possible salt value - computationally infeasible). Modern password hashing functions like bcrypt, Argon2id, and scrypt automatically generate and store the salt alongside the hash in a single string (e.g., $2b$12$saltvaluehashvalue), preventing salt reuse errors. The salt doesn't need to be secret - its security comes from uniqueness and randomness, not confidentiality. By forcing attackers to compute hashes individually rather than amortizing computation across millions of passwords, salting increases the time to crack a password database from hours to years.
Migration Considerations
CRITICAL: Adding salts to password hashes will invalidate all existing passwords unless you implement gradual migration.
What Breaks
- All existing passwords fail validation: Unsalted SHA-256 hashes won't match new salted bcrypt hashes
- User lockout: Every user will be unable to log in immediately after deployment
- Rainbow table attacks still possible during migration: Old unsalted hashes remain vulnerable until users log in
- Shared passwords visible: During migration, multiple users with same password still have identical hashes
Migration Approach
Dual-Read Strategy (Recommended)
Support both unsalted (legacy) and salted (new) password formats during transition:
- Add algorithm tracking: Store which hashing algorithm/format each password uses
-
Implement dual verification:
- Check if password hash uses new salted format (bcrypt/Argon2)
- If yes, verify using new algorithm
- If no (legacy unsalted), verify using old unsalted method
- On successful legacy verification, immediately upgrade to salted format
-
Database schema changes: Add columns for algorithm version and migration timestamp
-
Monitor migration timeline:
- Day 0: Deploy dual-read code
- Day 1-90: Users automatically upgrade as they log in
- Day 90: Check migration percentage
- Day 91: Send password reset to inactive users
- Day 120: Remove legacy support, force resets for stragglers
Big-Bang Migration (NOT Recommended)
Force all users to reset passwords via email. High user impact.
Rollback Procedures
If migration causes authentication failures:
- Immediate rollback: Revert application code to previous version (old hashes still work with dual-read)
- Database restore: Only needed if you accidentally overwrote hashes during big-bang migration
- Emergency password reset: Send reset emails to users reporting login issues
- Communication: Email affected users explaining temporary issue and resolution
Testing Recommendations
Pre-Production Testing:
- Test with production database copy in staging
- Verify unsalted passwords still work
- Verify successful login triggers upgrade to salted hash
- Verify upgraded passwords work on subsequent logins
- Test failed login attempts don't corrupt hashes
- Load test: Ensure migration logic doesn't slow authentication
- Verify migration tracking returns accurate percentages
Post-Deployment Monitoring:
- Monitor authentication success/failure rates
- Track migration progress daily
- Alert on authentication error rate > 5% increase
- Monitor password reset requests for unusual spikes
- Track average login response time
Key Metrics:
- Total active users
- Users migrated to salted hashes
- Users still on unsalted hashes
- Migration percentage
- Daily migration rate
- Estimated completion days
- Authentication success rate
- Password reset request rate
Security Checklist
- Password hashing uses bcrypt, Argon2, or scrypt
- Each password has unique random salt
- Salt is stored with hash
- Cost factor/iterations are appropriate
- Legacy passwords are automatically upgraded
- No plain text passwords in logs or storage