yiisoft/yii2 · error · InvalidArgumentException
Hash is invalid.
Error message
Hash is invalid.
What it means
Thrown by yii\base\Security::validatePassword() when the stored $hash does not look like a bcrypt hash: the code runs a format check (prefix $2a$/$2x$/$2y$, a two-digit cost between 04 and 30, then 22 salt characters from ./0-9A-Za-z) before delegating to password_verify(). Its purpose is to fail fast on malformed or foreign-algorithm hashes instead of passing garbage to crypt(). Any argon2i/argon2id hash, md5/sha1 digest, empty string, null, or truncated hash triggers it; even a valid bcrypt hash with cost 31 is rejected because the check caps cost at 30.
Source
Thrown at framework/base/Security.php:500
* Verifies a password against a hash.
* @param string $password The password to verify.
* @param string $hash The hash to verify the password against.
* @return bool whether the password is correct.
* @throws InvalidArgumentException on bad password/hash parameters.
* @see generatePasswordHash()
*/
public function validatePassword($password, $hash)
{
if (!is_string($password) || $password === '') {
throw new InvalidArgumentException('Password must be a string and cannot be empty.');
}
if (
!preg_match('/^\$2[axy]\$(\d\d)\$[\.\/0-9A-Za-z]{22}/', $hash, $matches)
|| $matches[1] < 4
|| $matches[1] > 30
) {
throw new InvalidArgumentException('Hash is invalid.');
}
return password_verify($password, $hash);
}
/**
* Generates a salt that can be used to generate a password hash.
*
* The PHP [crypt()](https://www.php.net/manual/en/function.crypt.php) built-in function
* requires, for the Blowfish hash algorithm, a salt string in a specific format:
* "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters
* from the alphabet "./0-9A-Za-z".
*
* @param int $cost the cost parameter
* @return string the random salt value.
* @throws InvalidArgumentException if the cost parameter is out of the range of 4 to 31.
* @deprecated since 2.0.55. This method is no longer used internally
* as [[generatePasswordHash()]] now relies on `password_hash()`. Will be removed in 2.2.View on GitHub (pinned to 66f00d18a2)
Solutions
- Inspect what is actually stored: var_dump(strlen($hash), substr($hash, 0, 7)); a Yii2-generated hash starts with $2y$10$.
- If hashes come from password_hash() with an argon2 algorithm, either regenerate them with Security::generatePasswordHash() (bcrypt) or bypass validatePassword() and call password_verify() directly, which accepts every password_hash() algorithm.
- For legacy md5/sha1 hashes, verify with the legacy algorithm, then transparently rehash to bcrypt on successful login (rehash-on-login migration).
- Widen the password_hash column to VARCHAR(255) and confirm the value is not null/truncated before validating.
Example fix
// before
if (Yii::$app->security->validatePassword($password, $user->password_hash)) { /* login */ }
// $user->password_hash is '$argon2id$v=19...' or an md5 digest -> InvalidArgumentException
// after (rehash-on-login for legacy hashes)
$hash = (string) $user->password_hash;
if (strncmp($hash, '$2y$', 4) === 0 || strncmp($hash, '$2a$', 4) === 0) {
$ok = Yii::$app->security->validatePassword($password, $hash);
} else {
$ok = hash_equals($hash, md5($password)); // legacy scheme
if ($ok) {
$user->password_hash = Yii::$app->security->generatePasswordHash($password);
$user->save(false);
}
} Defensive patterns
Strategy: validation
Validate before calling
// Run before validatePassword(): replicate the built-in format gate
function isBcryptHash($hash): bool
{
return is_string($hash)
&& preg_match('/^\$2[axy]\$(\d\d)\$[\.\/0-9A-Za-z]{22}/', $hash, $m) === 1
&& $m[1] >= 4 && $m[1] <= 30;
}
if (!isBcryptHash($user->password_hash)) {
// unusable stored credential: force reset or legacy rehash path, skip validatePassword()
} Type guard
function isBcryptHash($hash): bool { /* same regex check as validationCode */ } Try / catch
try {
$ok = Yii::$app->security->validatePassword($password, $hash);
} catch (\InvalidArgumentException $e) {
Yii::warning('Unusable stored hash for user ' . $user->id, 'security');
$ok = false; // failed login; optionally force password reset / rehash-on-login
} Prevention
- Generate every hash with Security::generatePasswordHash() so the format always matches the validator
- Store hashes in a VARCHAR(255) NOT NULL column so they cannot be null or truncated
- If another system writes argon2 hashes, do not route login through Security::validatePassword(); call password_verify() directly
- Add a data-quality check (hash prefix + length) when importing user rows
When it happens
Trigger: Calling Yii::$app->security->validatePassword($password, $hash) where $hash is: a PASSWORD_ARGON2ID/PASSWORD_ARGON2I hash produced by password_hash(); an md5()/sha1() digest from a legacy user table; null or '' from an empty DB column; a hash truncated by a varchar(32)/varchar(40) column; a bcrypt string with cost 31; or where the plain password was accidentally passed in place of the hash.
Common situations: Migrating a legacy user table (md5/sha1 passwords) into an app whose login uses Security::validatePassword(); another service or newer library wrote argon2 hashes while the Yii2 side only validates bcrypt; a password column that is too short or got truncated during an ETL; test fixtures seeded with fake hash strings like 'password'.
Related errors
- Password must be a string and cannot be empty.
- Cost must be between 4 and 31.
- Encryption requires the OpenSSL PHP extension
- {cipher} is not an allowed cipher
- Invalid parameters to hash_hkdf()
AI-assisted analysis of yiisoft/yii2@66f00d18a2 (2026-08-17).
Data as JSON: /api/errors/f44c40cc77b90a8d.
Report an issue: GitHub.