How to Store Passwords Securely in PHP (Password Hashing)
Keywords: password hashing PHP, store passwords securely, password_hash, password_verify, PHP security, best practices
Introduction
Storing user passwords in plain text is a critical security flaw that can lead to data breaches, identity theft, and massive reputational damage. PHP provides built‑in, battle‑tested functions that make password hashing simple and reliable. In this article we’ll explore the why and how of storing passwords securely in PHP, covering password_hash(), password_verify(), rehashing strategies, and common pitfalls to avoid.
Why Plain Text Passwords Are Dangerous
When a database is compromised, attackers can instantly read any plain text password. Even if you think users have weak passwords, the damage multiplies because many people reuse passwords across sites. Hashing transforms the password into a fixed‑length string that cannot be reversed (ideally), protecting users even if the database leaks.
PHP Password Hashing Functions
password_hash()
The password_hash() function creates a cryptographically strong hash using the Bcrypt algorithm by default (as of PHP 7.4+ it defaults to Argon2id if available). It automatically generates a unique salt, so you never have to manage salts yourself.
<?php
// Example: hashing a password
$plainPassword = 'My$3cureP@ssw0rd!';
$hash = password_hash($plainPassword, PASSWORD_DEFAULT);
// Store $hash in the database
?>
password_verify()
To check a user‑submitted password against the stored hash, use password_verify(). It extracts the algorithm and salt from the hash and performs the comparison safely.
<?php
// Example: verifying a password
$input = $_POST['password'];
$storedHash = $row['password_hash']; // fetched from DB
if (password_verify($input, $storedHash)) {
echo 'Login successful';
} else {
echo 'Invalid credentials';
}
?>
password_needs_rehash()
Algorithms improve over time. password_needs_rehash() tells you whether a stored hash should be regenerated with newer options (e.g., higher cost or a different algorithm).
<?php
if (password_needs_rehash($storedHash, PASSWORD_DEFAULT, ['cost' => 12])) {
$newHash = password_hash($input, PASSWORD_DEFAULT, ['cost' => 12]);
// Update the DB with $newHash
}
?>
Storing the Hash in the Database
Use a VARCHAR(255) column (or TEXT if you prefer) to store the hash. The length accommodates Bcrypt, Argon2i, and future algorithms.
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Never store the raw password, a static salt, or a “pepper” in the same table. If you choose to use a pepper (a secret value stored outside the DB, e.g., in an environment variable), prepend or append it **before** hashing:
<?php
$pepper = getenv('APP_PASSWORD_PEPPER'); // keep this secret!
$hash = password_hash($plainPassword . $pepper, PASSWORD_DEFAULT);
?>
Best Practices Checklist
- Always use
password_hash()withPASSWORD_DEFAULT(orPASSWORD_ARGON2IDif available). - Never create or store your own salts; let PHP handle it.
- Store only the hash (no plaintext, no separate salt, no pepper in the DB).
- Use
password_verify()for authentication. - Periodically call
password_needs_rehash()and update hashes when the cost factor or algorithm changes. - Enforce strong password policies (minimum length, mix of character types, blacklist common passwords).
- Use HTTPS everywhere to protect credentials in transit.
- Limit login attempts and implement account lockout or CAPTCHA to mitigate brute‑force attacks.
Common Pitfalls and How to Avoid Them
1. Using MD5, SHA1, or plain hash()
These algorithms are fast and vulnerable to rainbow‑table attacks. Always prefer password_hash(), which is deliberately slow.
2. Hard‑coding the cost factor
While a higher cost improves security, it also consumes CPU. Test your server’s performance and choose a cost that balances security and responsiveness (e.g., 10‑12 for Bcrypt).
3. Storing the hash in an insecure location
Ensure your database credentials are protected, use least‑privilege DB users, and keep backups encrypted.
Conclusion
Secure password storage in PHP is straightforward when you rely on the language’s built‑in functions. By using password_hash(), password_verify(), and password_needs_rehash(), you get a future‑proof solution that automatically handles salts, algorithm upgrades, and cost adjustments. Pair these functions with strong password policies, HTTPS, and rate limiting, and you’ll dramatically reduce the risk of credential theft.
Start implementing these practices today, and keep your users’ data safe.