Salt — random data added to a password before hashing to defeat rainbow tables
Beginner5 min read·eng-19-006
interviewsecurity
Concept
Salt — a random value added to a password BEFORE hashing. Each user gets a unique salt, stored alongside their hash. This defeats precomputed rainbow table attacks.
The problem salts solve — rainbow tables:
- Attackers precompute a table of hashes for millions of common passwords:
MD5("password") = 5f4dcc3b5aa765d61d8327deb882cf99. - If you store
MD5("password")in your database and it leaks, attackers look up the hash in their table → instant match. - With a salt:
MD5("password" + "x7Kq9mZ2")→ completely different hash. Attacker's rainbow table is useless — they'd need a separate table for every possible salt.
How salting works:
- Generate a random salt when creating a password:
$salt = bin2hex(random_bytes(16)). - Hash:
hash('sha256', $password . $salt). - Store BOTH the hash and the salt in the database.
- On login: retrieve the salt, re-hash the attempt, compare.
Modern approach: You don't manage salts manually. password_hash() (PHP) and Hash::make() (Laravel) handle salt generation and storage internally. The output (e.g., $2y$12$...) contains the algorithm, cost factor, AND salt encoded together.
Salt vs pepper:
- Salt: Unique per user, stored with the hash. Public (stored in DB).
- Pepper: One secret value added to ALL passwords, stored in application config/environment. Not stored in DB — if DB leaks without the app's pepper, hashes are harder to crack. But if config leaks, ALL passwords are compromised at once.
Never reuse salts: If two users have the same password AND the same salt, their hashes are identical — attackers can deduce duplicates. Unique salts per user prevent this.
Code Example
php
<?php
// HOW SALTS WORK (manual illustration — don't actually do this)
function manualSaltHash(string $password): string
{
$salt = bin2hex(random_bytes(16)); // 32 char random hex
$hash = hash('sha256', $password . $salt);
return $salt . ':' . $hash; // store both
}
function manualSaltVerify(string $password, string $stored): bool
{
[$salt, $hash] = explode(':', $stored);
return hash_equals($hash, hash('sha256', $password . $salt));
}
// Same password, different salts → DIFFERENT stored values (defeats rainbow tables)
$a = manualSaltHash('secret'); // "a3f9b2:8c91d4..."
$b = manualSaltHash('secret'); // "7e2k1m:4a82c1..." completely different!
// PHP's password_hash() does this automatically (and better)
$hash1 = password_hash('secret', PASSWORD_BCRYPT); // includes salt
$hash2 = password_hash('secret', PASSWORD_BCRYPT); // different salt, different output
// $hash1 !== $hash2 — even for the same password!
// The hash format includes everything (algo + cost + salt + hash):
// $2y$12$ {22 chars of salt} {31 chars of hash}
// bcrypt cost=12
password_verify('secret', $hash1); // true — extracts salt from $hash1, re-hashes
password_verify('wrong', $hash1); // false
// LARAVEL — uses password_hash() under the hood
$hash = Hash::make('mypassword');
// $hash = "$2y$12$IKqjH4ek..." — bcrypt format with embedded salt
Hash::check('mypassword', $hash); // true
// PEPPER (application-level secret — optional)
class PepperedHasher implements Hasher
{
private string $pepper;
public function __construct()
{
$this->pepper = config('hashing.pepper'); // secret in .env
}
public function make(string $value, array $options = []): string
{
return Hash::make($value . $this->pepper, $options);
}
public function check(string $value, string $hashedValue, array $options = []): bool
{
return Hash::check($value . $this->pepper, $hashedValue, $options);
}
}