skip to content

Password & Random APIs

password_hash and password_verify wrap bcrypt or Argon2 with salts and cost, and random_bytes gives you secure secrets. Interviewers test rehashing, the 72-byte limit and CSPRNG versus rand.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In PHP, what do password_hash() and password_verify() do, and what exactly should you store in the database?

level: juniorimportance: must knowfreq 62%

answer

  1. one-way hashing with a per-hash salt
  2. store the whole returned string, not the salt
  3. password_verify(plain, hash) returns bool
  4. PASSWORD_DEFAULT is currently bcrypt
  5. never compare hashes with ==

basics

~20 s

password_hash($plain, PASSWORD_DEFAULT) returns a self-describing string that embeds the algorithm, cost and a random salt; store that one string. password_verify($plain, $hash) rehashes the input the same way and returns a bool. Never generate the salt yourself or compare hashes manually.

solid answer

~50 s

`password_hash($password, PASSWORD_DEFAULT)` runs a **slow, salted, one-way** hash and returns a single string like `$2y$12$....` that **embeds the algorithm identifier, the cost, and a randomly generated salt**. You store exactly that string — there is no separate salt column, and you must not supply your own salt (the old `salt` option was removed in PHP 8.0). To check a login, call `password_verify($submitted, $storedHash)`: it reads the parameters out of the stored string, hashes the submitted password the same way, and compares in constant time, returning a `bool`. `PASSWORD_DEFAULT` is **currently bcrypt** but may change to a stronger algorithm in a future PHP, so store the hash in a column wide enough for growth (`VARCHAR(255)`). Since PHP 8.0 `password_hash` throws a `ValueError` on an invalid algorithm rather than returning `false`. The whole point of the API is that you never handle the salt, the comparison, or the format yourself.

go deeper

for a junior

Use password_hash to store and password_verify to check; store the whole returned string and never a separate salt.

for a middle

Explain that the hash embeds algorithm, cost and salt, why each call differs, and that PASSWORD_DEFAULT is currently bcrypt but may change.

for a senior

Reason about column width for algorithm agility, constant-time verification, and coexisting old and new hashes during upgrades.

for a principal

Set the org standard for algorithm and parameters and weigh Argon2id vs bcrypt against your login-traffic budget.

## What the two functions do Passwords must be stored so that a database leak does not reveal them, which means a **slow, salted, one-way** transformation — not a fast digest and not encryption. PHP's password API does this for you: - **`password_hash(string $password, string|int|null $algo, array $options = [])`** returns a hash string. With `PASSWORD_DEFAULT` (currently **bcrypt**), the result looks like: `$2y$12$Rnd22CharSaltHere22CharsThenThe31CharDigestValue...` The `$`-delimited fields are the **algorithm** (`2y` = bcrypt), the **cost** (`12`), and then a Base64 blob holding the **salt** and the **digest**. A fresh random salt is generated for every call, so hashing the same password twice gives two different strings — and that is correct. - **`password_verify(string $password, string $hash)`** returns a `bool`. It parses the algorithm, cost and salt out of `$hash`, recomputes the digest from `$password`, and compares them. You pass the *plaintext* the user just typed and the *stored hash*; you never split them apart. ## What you store — and what you must not do | Do | Don't | |---|---| | Store the **entire** returned string in one column | Store the salt in a separate column | | Use `VARCHAR(255)` so a future, longer algorithm still fits | Size the column to exactly 60 chars | | Let PHP generate the salt | Pass your own salt (the option was removed in 8.0) | | Compare with `password_verify()` | Compare with `==`, `===`, `strcmp`, or `md5($x) == $stored` | The salt is embedded precisely so the database needs only one column and `password_verify` can find it. Because the salt is per-hash, two users with the same password get different stored strings, which defeats precomputed-table attacks. ## Why `password_verify`, not a manual comparison Three reasons interviewers probe: 1. **The salt lives in the hash.** You cannot recompute the digest yourself without first parsing the algorithm and salt out of the stored string — which is exactly what `password_verify` does. 2. **Constant time.** `password_verify` compares in constant time, so it does not leak information through timing. A hand-rolled `==` on hashes is both wrong (see the loose-comparison bypass) and timing-unsafe. 3. **Algorithm agility.** Because the stored string is self-describing, the same `password_verify` call keeps working after you upgrade the algorithm or cost for new hashes; old and new hashes coexist. ## Choosing the algorithm `PASSWORD_DEFAULT` follows PHP's recommendation and today resolves to `PASSWORD_BCRYPT`. You can pin `PASSWORD_BCRYPT` explicitly, or choose `PASSWORD_ARGON2ID` (memory-hard, no 72-byte input limit) if your build has it. The *theory* of which algorithm and parameters to pick, and why a fast hash like SHA-256 is unfit for passwords, belongs to the security concept homes; the PHP-specific facts are the three functions, the self-describing string, and the "store the whole thing, never the salt" rule. Note the failure behaviour changed in PHP 8.0: `password_hash` now **throws a `ValueError`** for an unknown algorithm instead of returning `false`, so old `if ($h === false)` checks are dead code on modern PHP.

  • Why does hashing the same password twice give two different strings, and is that a bug?
    It is correct. `password_hash` generates a fresh random salt each call, and the salt is embedded in the output, so the two strings differ. `password_verify` still matches either against the original password because it reads each string's own salt. A per-hash salt is what stops one leaked rainbow table cracking every identical password at once.
  • Do you need a separate salt column in the users table?
    No. The salt is part of the string `password_hash` returns, so you store just that one column (typically `VARCHAR(255)`). Adding your own salt column, or passing a salt to `password_hash`, is a mistake — the salt option was deprecated in 7.0 and removed in 8.0.
  • Is `PASSWORD_ARGON2ID` better than the default?
    Argon2id is memory-hard and has no 72-byte input limit, which are real advantages, but 'better' depends on your build and CPU/memory budget — the algorithm comparison is a security-design question. In PHP terms, either is a valid `$algo`; use `PASSWORD_DEFAULT` unless you have a specific reason and the argon2 build support.

The stored string is like a sealed envelope that also prints its own combination on the outside: password_verify reads the combination (algorithm, cost, salt) off the envelope to re-check the contents, so you never keep the combination in a separate drawer.

saying these in an interview costs you the question

  • you must generate and store your own salt
  • compare stored hashes with == or ===
  • the same password always hashes to the same string
  • PASSWORD_DEFAULT is guaranteed to always be bcrypt
  • password_hash returns false on failure so check === false
  • store only the digest and keep the salt in another column
open as a page

In PHP, which functions generate a secure token, and why are rand() and mt_rand() the wrong choice for one?

level: middleimportance: must knowfreq 48%

basics

~20 s

Use random_bytes($len) for a token (then bin2hex/base64 to make it printable) and random_int($min, $max) for a secure number; both draw from the OS CSPRNG and throw Random\RandomException on failure. rand() and mt_rand() are fast but predictable, so an attacker can reproduce their output.

open as a page

In PHP, what is bcrypt's 72-byte limit in password_hash(), and how does it bite long or multibyte passwords?

level: middleimportance: should knowfreq 30%

basics

~20 s

With PASSWORD_BCRYPT, password_hash() silently truncates the password to 72 bytes, so anything past byte 72 is ignored. Because it counts bytes, multibyte UTF-8 characters use several each. Two long passwords sharing a 72-byte prefix verify as equal. ARGON2ID has no such limit.

open as a page

You inherit a forum whose users table stores raw MD5 password hashes; how do you migrate it to password_hash() without resetting everyone's password?

level: seniorimportance: should knowfreq 38%

basics

~20 s

You cannot batch-convert MD5 hashes because you lack the plaintext. Migrate on login: detect a legacy row, verify with the old scheme, then rehash the plaintext with password_hash and store it. Optionally wrap every existing MD5 in password_hash immediately so all rows get a slow layer at once.

open as a page

In PHP, why should a login flow call password_needs_rehash(), and what does the PHP 8.4 bcrypt cost change mean for existing hashes?

level: seniorimportance: should knowfreq 42%

basics

~20 s

You can only upgrade a hash when you hold the plaintext, which is at login. After a successful password_verify, call password_needs_rehash($hash, PASSWORD_DEFAULT); if it returns true, rehash the plaintext and store it. PHP 8.4 raised bcrypt's default cost from 10 to 12, so pre-8.4 hashes need a rehash.

open as a page