Passwords in the Crosshairs: Why Old Methods No Longer Work
\nImagine: you are a developer writing a registration system. It seems simple, right? Take a password, run it through md5, save it to the database. But in 2025, this approach is a direct path to vulnerability. Modern attacks, GPU brute-forcing, and rainbow tables turn weak hashes into dust in seconds. The question from Stack Overflow with thousands of views — "Secure hash and salt for PHP passwords" — remains relevant, and we will settle it once and for all.
\nIn this article, you will learn how to properly hash passwords in PHP using modern algorithms, built-in functions, and salt. We adapt the best community answers for educational content and show you how to protect users even in 2025.
\n\nWhy md5 and sha1 are Evil
\nIf you are still using md5 or sha1 to store passwords — stop. These algorithms were designed for speed, not security. In 2025, a GPU can brute-force billions of md5 hashes per second. Adding salt (a random string) does not help: rainbow tables are bypassed, but the speed remains fatal.
\n- \n
- Md5: 2^128 variants — cracked in minutes. \n
- Sha1: slightly better, but also outdated. \n
- Salt without slowing down: if the hash is fast, salt only delays the attack. \n
Instead, you need algorithms with "slowness" — those that require time and memory. This is exactly what password_hash uses.
password_hash and password_verify: Built-in Solution from PHP
\nSince PHP 5.5, two functions have solved 90% of password security problems: password_hash and password_verify. They automatically generate a cryptographically secure salt and use algorithms resistant to brute-force. In 2025, this is the standard, and no homemade solutions!
How Does It Work?
\nThe password_hash function takes a password and an algorithm (default is bcrypt, but you can specify argon2). It returns a string that already contains the salt and parameters. You do not need to store the salt separately — it is inside the hash.
<?php\n// Hash the password\n$password = "SuperSecret123!";\n$hash = password_hash($password, PASSWORD_DEFAULT);\n\necho $hash; // $2y$10$... (60 characters)\n?>\n\nNow let's verify the password during login:
\n<?php\n$inputPassword = "SuperSecret123!";\n$storedHash = "$2y$10$..."; // from the database\n\nif (password_verify($inputPassword, $storedHash)) {\n echo "Password is correct!";\n} else {\n echo "Invalid password.";\n}\n?>\n\nIt's ingeniously simple. password_verify automatically extracts the salt from the hash and checks the password. No additional steps needed.
Algorithm Choice: bcrypt vs argon2
\nIn 2025, you have two main options:
\n- \n
- Bcrypt (PASSWORD_BCRYPT): time-tested, used by default. Resistant to attacks, but requires configuring the cost factor. \n
- Argon2 (PASSWORD_ARGON2I or PASSWORD_ARGON2ID): winner of the Password Hashing Competition. More modern, requires more memory, making GPU attacks harder. Recommended for new projects. \n
Example with argon2:
\n<?php\n$hash = password_hash($password, PASSWORD_ARGON2ID, [\n 'memory_cost' => 1 << 17, // 128 MB\n 'time_cost' => 4,\n 'threads' => 2\n]);\n?>\n\nThe memory_cost parameter is the amount of memory in kilobytes. The higher it is, the more secure, but slower. Test on your server for production.
Salt: How It Works and Why You Don't Need It Separately
\nMany beginners ask: "How do I add salt manually?" The answer: don't. password_hash generates 22 characters of random salt (for bcrypt