Password Hashing Best Practices
Korrekt password hashing beskytter bruger-credentials selv ved databrud ved at bruge irreversible, computationally expensive algoritmer.
Om Truslen
Password hashing er fundamentalt for at beskytte brugerkonti, men mange udviklere implementerer det forkert. Når en database kompromitteres, er korrekt hashede passwords den sidste forsvarslinje mod account takeover. Moderne password hashing kræver algoritmer specifikt designet til formålet - bcrypt, scrypt eller Argon2 - ikke generiske hash-funktioner som MD5 eller SHA. Disse algoritmer er computationally expensive og memory-hard, hvilket gør rainbow table-angreb og brute force-crackning ekstremt langsomme og dyre. Statistikker viser at databrud der afslører plaintext eller svagt hashede passwords resulterer i 100% account compromise, mens proper hashing med salt reducerer succesraten dramatisk. Argon2 blev kåret som vinder af Password Hashing Competition i 2015 og anbefales af sikkerhedseksperter. Cost factors skal justeres over tid for at holde trit med hardware-forbedringer.
Key Points
- ✓Brug password_hash() med PASSWORD_ARGON2ID eller PASSWORD_BCRYPT - aldrig MD5, SHA1 eller SHA256
- ✓Lad password_hash() automatisk generere kryptografisk sikre salts
- ✓Brug password_verify() til at validere passwords - aldrig manual sammenligning
- ✓Implementer cost factors der gør hashing langsom nok (100-350ms på din hardware)
- ✓Brug password_needs_rehash() til at opdatere gamle hashes når algoritmer forbedres
- ✓Gem aldrig passwords i plaintext - heller ikke midlertidigt
- ✓Brug pepper (server-side secret) som ekstra lag af beskyttelse
- ✓Implementer minimum password-længde på 12+ tegn
- ✓Hash passwords før de sendes til database for at undgå logging i plaintext
- ✓Brug timing-safe comparison ved password verification
- ✓Overvej at bruge Argon2id for maksimal sikkerhed mod både side-channel og GPU-angreb
- ✓Opdater regelmæssigt cost factors for at holde trit med hardware-forbedringer
- ✓Kombinér med andre sikkerhedsforanstaltninger som rate limiting og MFA
- ✓Log aldrig passwords - heller ikke ved fejl eller debugging
Sårbar Kode (UNDGÅ)
Brug ALDRIG denne kode i produktion!
<?php
// SÅRBAR KODE - Brug ALDRIG dette i produktion!
// 1. KRITISK FEJL: Plaintext password storage
$username = $_POST['username'];
$password = $_POST['password'];
$query = "INSERT INTO users (username, password) VALUES ('$username', '$password')";
mysqli_query($conn, $query);
// ALDRIG acceptabelt! Passwords skal ALTID hashes
// 2. KRITISK FEJL: MD5 hashing (kan crackeres på sekunder)
$password = $_POST['password'];
$hashed = md5($password);
$query = "INSERT INTO users (password) VALUES ('$hashed')";
mysqli_query($conn, $query);
// MD5 er IKKE en password-hashing algoritme!
// Rainbow tables kan reverse MD5 hashes øjeblikkeligt
// 3. KRITISK FEJL: SHA256 uden salt
$password = $_POST['password'];
$hashed = hash('sha256', $password);
$query = "INSERT INTO users (password) VALUES ('$hashed')";
mysqli_query($conn, $query);
// SHA256 er for hurtig og uden salt sårbar for rainbow tables
// 4. FORKERT: Selvlavet salt der ikke er random
$password = $_POST['password'];
$username = $_POST['username'];
$salt = $username; // DÅRLIG IDÉ!
$hashed = hash('sha256', $password . $salt);
$query = "INSERT INTO users (password, salt) VALUES ('$hashed', '$salt')";
mysqli_query($conn, $query);
// Bruger username som salt er forudsigeligt
// 5. FORKERT: Utilstrækkelig salt-generering
$password = $_POST['password'];
$salt = rand(1000, 9999); // Kun 9000 muligheder!
$hashed = hash('sha256', $password . $salt);
$query = "INSERT INTO users (password, salt) VALUES ('$hashed', '$salt')";
mysqli_query($conn, $query);
// rand() er ikke kryptografisk sikkert
// 6. FORKERT: Logging af passwords
$password = $_POST['password'];
error_log("User login attempt with password: $password"); // MEGET FARLIGT!
$hashed = password_hash($password, PASSWORD_DEFAULT);
// Selvom password hashes, er det nu logget i plaintext
// 7. FORKERT: Email af password
$password = generate_random_password();
$hashed = password_hash($password, PASSWORD_DEFAULT);
// Sender password via email
mail($user_email, "Your password", "Your password is: $password");
// Email er usikkert og passwords bør aldrig sendes
// 8. FORKERT: Insecure password comparison
$input_password = $_POST['password'];
$stored_hash = getPasswordFromDB($user_id);
if (hash('sha256', $input_password) == $stored_hash) {
// Login successful
}
// Bruger == i stedet for timing-safe comparison
// Sårbar for timing attacks
// 9. FORKERT: Genbruger samme salt for alle passwords
$global_salt = "mysecret123";
$password = $_POST['password'];
$hashed = hash('sha256', $password . $global_salt);
// Alle passwords bruger samme salt - meget usikkert!
// 10. FORKERT: Password hashing med for lav cost
$password = $_POST['password'];
$hashed = password_hash($password, PASSWORD_BCRYPT, ['cost' => 4]);
// Cost 4 er ALT for lavt og kan crackeres hurtigt
// Default er 10, men burde være højere på moderne hardware
?>Sikker Kode (ANBEFALET)
Best practices for sikker implementation
<?php
// SIKKER KODE - Best practices for password hashing
// 1. ANBEFALET: Brug password_hash() med Argon2id
$password = $_POST['password'] ?? '';
// Validér password strength først
if (strlen($password) < 12) {
die("Password must be at least 12 characters");
}
// Argon2id er den mest sikre algoritme (modstår både side-channel og GPU-angreb)
$hashed_password = password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536, // 64 MB
'time_cost' => 4, // 4 iterations
'threads' => 3 // 3 parallel threads
]);
// Gem i database
$stmt = $pdo->prepare("INSERT INTO users (username, password, created_at) VALUES (?, ?, NOW())");
$stmt->execute([$username, $hashed_password]);
// password_hash() genererer automatisk et kryptografisk sikkert salt
// og inkluderer det i output-stringen
// 2. ALTERNATIV: Bcrypt (god kompatibilitet)
$password = $_POST['password'] ?? '';
// Bcrypt med passende cost factor
$hashed_password = password_hash($password, PASSWORD_BCRYPT, [
'cost' => 12 // Juster baseret på din servers ydelse
]);
// Test hashing-tid (burde være 100-350ms)
$start = microtime(true);
password_hash('test_password', PASSWORD_BCRYPT, ['cost' => 12]);
$time = (microtime(true) - $start) * 1000;
echo "Hashing time: {$time}ms\n";
// 3. Password verification med password_verify()
$input_password = $_POST['password'] ?? '';
$username = $_POST['username'] ?? '';
// Hent bruger fra database
$stmt = $pdo->prepare("SELECT id, username, password FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
// Timing-safe failure
password_hash($input_password, PASSWORD_ARGON2ID);
die("Invalid credentials");
}
// password_verify() er timing-safe
if (password_verify($input_password, $user['password'])) {
// Login successful
echo "Login successful";
// Check om password skal rehashes
if (password_needs_rehash($user['password'], PASSWORD_ARGON2ID, [
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 3
])) {
// Rehash med nye parametre
$new_hash = password_hash($input_password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 3
]);
$stmt = $pdo->prepare("UPDATE users SET password = ? WHERE id = ?");
$stmt->execute([$new_hash, $user['id']]);
}
} else {
die("Invalid credentials");
}
// 4. AVANCERET: Password hashing med pepper
class SecurePasswordHasher {
private $pepper;
public function __construct() {
// Pepper gemmes som miljøvariabel, IKKE i database
$this->pepper = getenv('PASSWORD_PEPPER');
if (!$this->pepper) {
throw new Exception("Pepper not configured");
}
}
public function hash($password) {
// Kombiner password med pepper før hashing
$peppered = hash_hmac('sha256', $password, $this->pepper);
return password_hash($peppered, PASSWORD_ARGON2ID, [
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 3
]);
}
public function verify($password, $hash) {
$peppered = hash_hmac('sha256', $password, $this->pepper);
return password_verify($peppered, $hash);
}
}
$hasher = new SecurePasswordHasher();
// Registration
$password = $_POST['password'] ?? '';
$hashed = $hasher->hash($password);
$stmt = $pdo->prepare("INSERT INTO users (password) VALUES (?)");
$stmt->execute([$hashed]);
// Login
$input_password = $_POST['password'] ?? '';
$stmt = $pdo->prepare("SELECT password FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && $hasher->verify($input_password, $user['password'])) {
echo "Login successful";
}
// 5. Password strength validation
function validatePasswordStrength($password) {
$errors = [];
if (strlen($password) < 12) {
$errors[] = "Password must be at least 12 characters";
}
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = "Password must contain at least one uppercase letter";
}
if (!preg_match('/[a-z]/', $password)) {
$errors[] = "Password must contain at least one lowercase letter";
}
if (!preg_match('/[0-9]/', $password)) {
$errors[] = "Password must contain at least one number";
}
if (!preg_match('/[^A-Za-z0-9]/', $password)) {
$errors[] = "Password must contain at least one special character";
}
// Check for common passwords
$common_passwords = file('common-passwords.txt', FILE_IGNORE_NEW_LINES);
if (in_array(strtolower($password), $common_passwords, true)) {
$errors[] = "Password is too common";
}
return $errors;
}
// 6. Secure password reset med temporary token
function generatePasswordResetToken() {
return bin2hex(random_bytes(32));
}
function createPasswordResetToken($user_id) {
$token = generatePasswordResetToken();
$token_hash = password_hash($token, PASSWORD_ARGON2ID);
$expires = date('Y-m-d H:i:s', strtotime('+1 hour'));
$stmt = $pdo->prepare(
"INSERT INTO password_reset_tokens (user_id, token_hash, expires_at)
VALUES (?, ?, ?)"
);
$stmt->execute([$user_id, $token_hash, $expires]);
return $token; // Send dette via email
}
?>Almindelige Fejl
- ⚠At bruge MD5, SHA1 eller SHA256 til password hashing - disse er designet til hastighed, ikke sikkerhed
- ⚠At implementere egen salt-generering i stedet for at bruge password_hash() der gør det automatisk
- ⚠At bruge for lav cost factor der gør hashing for hurtig og sårbar for brute force
- ⚠At sammenligne passwords med == eller === i stedet for password_verify() der er timing-safe
- ⚠At ikke opdatere gamle password hashes når algoritmer forbedres
- ⚠At logge passwords i plaintext, selv midlertidigt under development eller debugging
Forebyggelsesteknikker
- 🛡️Brug eksklusivt password_hash() med PASSWORD_ARGON2ID eller PASSWORD_BCRYPT
- 🛡️Konfigurer passende cost factors baseret på din servers ydelse (mål: 100-350ms)
- 🛡️Brug password_verify() for al password-validering - aldrig manual comparison
- 🛡️Implementer password_needs_rehash() for at opdatere gamle hashes automatisk
- 🛡️Tilføj pepper (server-side secret) som ekstra lag af beskyttelse
- 🛡️Implementer stærke password policies der kræver kompleksitet og længde
- 🛡️Brug password strength meters til at guide brugere
- 🛡️Kombiner med rate limiting og account lockout for fuld beskyttelse
Testing Metoder
- 🔍Verificer at passwords gemmes korrekt hashet i databasen (ikke plaintext)
- 🔍Test at password_verify() accepterer korrekte passwords og afviser forkerte
- 🔍Mål hashing-tid for at sikre passende cost factors
- 🔍Verificer at password_needs_rehash() fungerer korrekt
- 🔍Test at samme password resulterer i forskellige hashes (salt fungerer)
Quick Info
- ⚠Rainbow Table Attacks - Precomputed hash-lookups
- ⚠Brute Force Attacks - Systematisk gætning af passwords
- ⚠Dictionary Attacks - Prøvning af almindelige passwords
⚠️Sikkerhedsadvarsel
Sikkerhedssårbarheder kan have alvorlige konsekvenser. Test altid grundigt i et sikkert miljø og implementer alle anbefalede forebyggelsesteknikker.