Salted Hashing
debt(d5/e3/b3/t7)
Closest to 'specialist tool catches' (d5), semgrep/psalm/phpstan rules can match md5($salt.$password) or sha1(SALT.$password) patterns, but it won't fail compilation and basic linters miss it.
Closest to 'simple parameterised fix' (e3), quick_fix says swap manual salted hash for password_hash() — usually localized to auth/user model with a verification migration path for existing hashes.
Closest to 'localised tax' (b3), password hashing lives in the auth component; choice doesn't pervade the whole codebase but does require consistent verify logic wherever credentials are checked.
Closest to 'serious trap' (t7), misconception explicitly states devs believe a shared salt is acceptable — the 'obvious' DIY salting (global constant, username as salt, fast hash + salt) is wrong in multiple non-obvious ways.
Also Known As
TL;DR
Explanation
A salt is a cryptographically random value generated per-user and stored alongside the hash. Salting means two identical passwords produce completely different hashes, so an attacker who obtains the database cannot use precomputed rainbow tables or spot users with the same password. Modern PHP's password_hash() handles salting automatically — never implement manual salting. The salt does not need to be secret, only unique. bcrypt, Argon2id, and scrypt all incorporate salting internally.
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Using a global application-wide salt instead of a per-password unique random salt.
- Storing the salt in a separate column and not including it in the hash input consistently.
- Using a predictable salt like the username or user ID — defeats the purpose.
- Using salted MD5 or SHA1 — these are still fast-hashable; use password_hash() with bcrypt/argon2 instead.
Code Examples
// Unsalted — rainbow tables crack these instantly
\$hash = sha1(\$password);
// Shared salt — compromise of one reveals all
\$hash = sha1(\$globalSalt . \$password);
// PHP generates a unique per-password salt automatically inside password_hash
\$hash = password_hash(\$password, PASSWORD_ARGON2ID);
// The salt is embedded in the hash string — no separate column needed
// Verify — salt extracted automatically from hash string
if (password_verify(\$input, \$hash)) { /* match */ }
// Manual salting (if implementing yourself — rarely necessary)
\$salt = bin2hex(random_bytes(16)); // unique per user
\$hash = hash('sha256', \$salt . \$password);
// Store \$salt alongside \$hash — never store the password