PHP 8.0: crypt() function requires $salt parameter

Version8.0
TypeChange

PHP crypt() function requires its $saltparameter to be passed in PHP 8.0 and forward, changing from its prior behavior of raising a notice when the parameter was not passed.

crypt() function is largely replaced by password_hash() function and provides secure defaults. Not passing a password salt to the crypt() function makes PHP generate one, but due to historical reasons, this salt is insecure by today's security standards. Further, the salt had to adhere to certain patterns to switch certain hashing mechanisms.

Unless your application requires compatibility with other APIs that need to store crypt()-compatible hashes, consider using password_hash/verify/needs_rehash() functions.

Using crypt() function without a specific $salt parameter makes PHP pick a hashing algorithm based on the availability of the mechanisms in the system it runs on, and thus can lead to unexpected (and often insecure) results.

$hash = crypt('p4ssw0rd');

The above snippet results in a PHP notice since PHP >=5.6 and PHP < 8.0:

Notice: crypt(): No salt parameter was specified. You must use a randomly generated salt and a strong hash function to produce a secure hash. in ... on line ...

Since PHP 8.0, the second parameter is required, and thus will result in an ArgumentCountError exception.

ArgumentCountError: crypt() expects exactly 2 parameters, 1 given

Transition to PHP 8

It might be possible to detect the salt PHP automatically generated prior to PHP 8.0, and provide the very same to the crypt() function, so you can verify the password before moving to password_hash() function.

When the $salt parameter is not provided to the crypt() function, a random salt is generated, and returned along the the returned hash.

crypt('p4ssw0rd');
// $1$N1EHVYY9$qz.BE7oyWWkD85o5uCvO8/

In the example above, $1$N1EHVYY9$ (different in each invocation) is the random salt, and $1$ indicates that the hash is of CRYPT_MD5 type. Various mechanisms such as CRYPT_SHA512 orCRYPT_BLOWFISH will contain different identifiers, but they all will follow the same pattern, that can be captured with the Regular Expression below:

(^\$.+\$).+

Until your application is using password_hash(), you will be able to temporarily authenticate users

$pattern = '/(^\$.+\$).+/';
$stored_hash = '$1$N1EHVYY9$qz.BE7oyWWkD85o5uCvO8/';
preg_match($pattern, $stored_hash, $matches);
if (empty($matches[1])) {
    return false;// Unknown salt pattern.
}

$hash = crypt('p4ssw0rd', $matches[1]);
if (!hash_equals($hash, $stored_hash)) {
    return false; // Invalid password.
}

$stored_hash = password_hash('p4ssw0rd', PASSWORD_DEFAULT);
// Now, store $stored_hash back.
...

Backwards compatibility impact

What was previously a PHP notice due to missing second parameter to crypt() will result in an ArgumentCountError exception in PHP 8.0.

You can continue to use the crypt() function until you migrate to use password_* functions, as long as the $salt parameter is passed, or extracted from the stored hash as mentioned in the example above.

password_* functions are not affected by this change.


Implementation RFC (2013)