Intermediate

Common Hashing Mistakes

Security pitfalls and best practices for implementing hash-based systems.

1. Using Fast Hashes for Passwords

The Mistake

// WRONG: Fast hash for passwords password_hash = SHA256(password)

SHA-256 can hash billions of passwords per second on modern GPUs. Attackers can brute-force weak passwords in minutes.

The Fix

// CORRECT: Slow, memory-hard password hash password_hash = argon2id(password, salt, cost_params)

Use specialized password hashing algorithms (Argon2, bcrypt, scrypt) that are intentionally slow and memory-intensive to resist brute-force attacks.

Rule: If it's fast, it's wrong for passwords.

2. Not Using Salt

The Mistake

// WRONG: No salt hash = SHA256(password)

Identical passwords produce identical hashes. Attackers can use rainbow tables (precomputed hash databases) to crack millions of passwords instantly.

The Fix

// CORRECT: Unique salt per password salt = generate_random_salt() hash = argon2id(password + salt) store(salt, hash) // Store both

Generate a unique random salt for each password. Even if two users have the same password, their hashes will be different. This defeats rainbow tables and makes parallel cracking impossible.

Salt should be at least 16 bytes of cryptographically random data.

3. Using the Same Salt for Everyone

The Mistake

// WRONG: Global salt GLOBAL_SALT = "myapp_salt_2024" hash = SHA256(password + GLOBAL_SALT)

A global salt (pepper) doesn't prevent identical passwords from having identical hashes. Attackers can still use rainbow tables once they discover the global salt.

The Fix

// CORRECT: Unique salt per user user_salt = generate_random_salt() // Different for each user hash = argon2id(password, user_salt)

Each password must have its own unique salt. You can optionally add a global pepper (secret key) for defense-in-depth, but it doesn't replace per-user salts.

4. Rolling Your Own Crypto

The Mistake

// WRONG: Custom hash function function myHash(data) let hash = 0; for (let char of data) hash = (hash * 31 + char.charCodeAt(0)) % 1000000; return hash;

Custom hash functions are almost always broken. Cryptography is extremely difficult to get right, and subtle flaws can completely compromise security.

The Fix

// CORRECT: Use standard library import hashlib hash = hashlib.sha256(data).hexdigest()

Always use well-tested, standard cryptographic libraries. These implementations have been reviewed by experts and tested extensively.

Rule: Don't implement your own cryptographic primitives. Ever.

5. Comparing Hashes with == (Timing Attacks)

The Mistake

// WRONG: Timing attack vulnerable if (computed_hash == stored_hash) authenticate_user();

String comparison stops at the first mismatch. Attackers can measure response times to guess the hash byte-by-byte, potentially recovering authentication tokens or API keys.

The Fix

// CORRECT: Constant-time comparison import hmac if (hmac.compare_digest(computed_hash, stored_hash)) authenticate_user();

Use constant-time comparison functions that always take the same amount of time regardless of where the mismatch occurs. This prevents timing attacks.

6. Truncating Hashes

The Mistake

// WRONG: Truncating for "convenience" short_hash = SHA256(data).substring(0, 8)

Truncating hashes dramatically reduces security. An 8-character hex string is only 32 bits, making collisions trivial to find (birthday attack at 2^16 operations).

The Fix

// CORRECT: Use full hash full_hash = SHA256(data) // All 256 bits

Always use the full hash output. If you need shorter identifiers, use a hash function designed for shorter outputs (like BLAKE2 with configurable length) rather than truncating.

Exception: Git uses truncated hashes for display, but stores full hashes internally.

7. Not Handling Hash Collisions

The Mistake

// WRONG: Assuming hashes are unique file_id = MD5(file_content) database[file_id] = file_content // Overwrites on collision

Even with strong hash functions, collisions are theoretically possible (though astronomically unlikely for SHA-256). Systems should handle collisions gracefully.

The Fix

// CORRECT: Verify content on collision file_id = SHA256(file_content) if (file_id in database) if (database[file_id] != file_content) handle_collision() // Extremely rare but possible

When using hashes as identifiers, verify the actual content matches on collision. This is how Git handles the theoretical possibility of SHA-1 collisions.

8. Using MD5 or SHA-1 for Security

The Mistake

// WRONG: Broken algorithms signature = MD5(document + secret_key) certificate_fingerprint = SHA1(certificate)

MD5 and SHA-1 are cryptographically broken. Attackers can create collisions, allowing them to forge signatures and certificates.

The Fix

// CORRECT: Modern algorithms signature = HMAC_SHA256(document, secret_key) certificate_fingerprint = SHA256(certificate)

Use SHA-256 or newer algorithms. MD5 and SHA-1 are only acceptable for non-security purposes like detecting accidental corruption.

See our guide: Why Never Use MD5

9. Length Extension Attacks

The Mistake

// WRONG: Vulnerable to length extension auth_token = SHA256(secret_key + user_data)

SHA-256 and SHA-512 are vulnerable to length extension attacks. Attackers can append data to the message and compute a valid hash without knowing the secret key.

The Fix

// CORRECT: Use HMAC auth_token = HMAC_SHA256(secret_key, user_data)

Use HMAC (Hash-based Message Authentication Code) for authenticated hashing. HMAC is specifically designed to prevent length extension attacks.

Alternative: Use SHA-3 or BLAKE2, which aren't vulnerable to length extension.

10. Not Versioning Hash Algorithms

The Mistake

// WRONG: No algorithm identifier store_hash(hash_value)

When you need to upgrade hash algorithms (e.g., MD5 → SHA-256), you can't tell which algorithm was used for existing hashes.

The Fix

// CORRECT: Include algorithm identifier hash_string = "$sha256$" + hash_value // or use standard formats like: // $argon2id$v=19$m=65536,t=3,p=4$salt$hash

Store the algorithm identifier with the hash. This allows graceful migration to new algorithms while maintaining backward compatibility.

Best Practices Summary

✓ Do This

  • -Use Argon2id or bcrypt for passwords
  • -Generate unique random salts (16+ bytes)
  • -Use SHA-256 or newer for general hashing
  • -Use HMAC for authenticated hashing
  • -Use constant-time comparison for security checks
  • -Store algorithm identifiers with hashes
  • -Use standard cryptographic libraries
  • -Keep hash outputs at full length

✗ Don't Do This

  • -Use fast hashes (SHA-256, BLAKE3) for passwords
  • -Hash passwords without salt
  • -Use the same salt for multiple users
  • -Use MD5 or SHA-1 for security
  • -Implement your own hash functions
  • -Truncate hashes for convenience
  • -Use == for hash comparison in security contexts
  • -Concatenate secrets with data (use HMAC instead)

Testing Your Implementation

Security checklist:
  • Run your code through a security linter
  • Verify you're using the latest version of crypto libraries
  • Test with known test vectors from algorithm specifications
  • Verify salts are unique per user in your database
  • Check that password hashing takes at least 100ms
  • Audit all hash comparisons for timing attacks
  • Have a security expert review your implementation

Official Resources

Related Guides