{"record":{"id":"f44c40cc77b90a8d","repo":"yiisoft/yii2","slug":"hash-is-invalid","errorCode":null,"errorMessage":"Hash is invalid.","messagePattern":"Hash is invalid\\.","errorType":"exception","errorClass":"InvalidArgumentException","httpStatus":null,"severity":"error","filePath":"framework/base/Security.php","lineNumber":500,"sourceCode":"     * Verifies a password against a hash.\n     * @param string $password The password to verify.\n     * @param string $hash The hash to verify the password against.\n     * @return bool whether the password is correct.\n     * @throws InvalidArgumentException on bad password/hash parameters.\n     * @see generatePasswordHash()\n     */\n    public function validatePassword($password, $hash)\n    {\n        if (!is_string($password) || $password === '') {\n            throw new InvalidArgumentException('Password must be a string and cannot be empty.');\n        }\n\n        if (\n            !preg_match('/^\\$2[axy]\\$(\\d\\d)\\$[\\.\\/0-9A-Za-z]{22}/', $hash, $matches)\n            || $matches[1] < 4\n            || $matches[1] > 30\n        ) {\n            throw new InvalidArgumentException('Hash is invalid.');\n        }\n\n        return password_verify($password, $hash);\n    }\n\n    /**\n     * Generates a salt that can be used to generate a password hash.\n     *\n     * The PHP [crypt()](https://www.php.net/manual/en/function.crypt.php) built-in function\n     * requires, for the Blowfish hash algorithm, a salt string in a specific format:\n     * \"$2a$\", \"$2x$\" or \"$2y$\", a two digit cost parameter, \"$\", and 22 characters\n     * from the alphabet \"./0-9A-Za-z\".\n     *\n     * @param int $cost the cost parameter\n     * @return string the random salt value.\n     * @throws InvalidArgumentException if the cost parameter is out of the range of 4 to 31.\n     * @deprecated since 2.0.55. This method is no longer used internally\n     * as [[generatePasswordHash()]] now relies on `password_hash()`. Will be removed in 2.2.","sourceCodeStart":482,"sourceCodeEnd":518,"githubUrl":"https://github.com/yiisoft/yii2/blob/66f00d18a29b520f85e8e8f1e32d1e7e7b556cac/framework/base/Security.php#L482-L518","documentation":"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.","triggerScenarios":"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.","commonSituations":"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'.","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."],"exampleFix":"// before\nif (Yii::$app->security->validatePassword($password, $user->password_hash)) { /* login */ }\n// $user->password_hash is '$argon2id$v=19...' or an md5 digest -> InvalidArgumentException\n\n// after (rehash-on-login for legacy hashes)\n$hash = (string) $user->password_hash;\nif (strncmp($hash, '$2y$', 4) === 0 || strncmp($hash, '$2a$', 4) === 0) {\n    $ok = Yii::$app->security->validatePassword($password, $hash);\n} else {\n    $ok = hash_equals($hash, md5($password)); // legacy scheme\n    if ($ok) {\n        $user->password_hash = Yii::$app->security->generatePasswordHash($password);\n        $user->save(false);\n    }\n}","handlingStrategy":"validation","validationCode":"// Run before validatePassword(): replicate the built-in format gate\nfunction isBcryptHash($hash): bool\n{\n    return is_string($hash)\n        && preg_match('/^\\$2[axy]\\$(\\d\\d)\\$[\\.\\/0-9A-Za-z]{22}/', $hash, $m) === 1\n        && $m[1] >= 4 && $m[1] <= 30;\n}\n\nif (!isBcryptHash($user->password_hash)) {\n    // unusable stored credential: force reset or legacy rehash path, skip validatePassword()\n}","typeGuard":"function isBcryptHash($hash): bool { /* same regex check as validationCode */ }","tryCatchPattern":"try {\n    $ok = Yii::$app->security->validatePassword($password, $hash);\n} catch (\\InvalidArgumentException $e) {\n    Yii::warning('Unusable stored hash for user ' . $user->id, 'security');\n    $ok = false; // failed login; optionally force password reset / rehash-on-login\n}","preventionTips":["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"],"tags":["php","yii2","security","bcrypt","password","authentication"],"backgroundTag":"invalid-password-hash-format","analyzedSha":"66f00d18a29b520f85e8e8f1e32d1e7e7b556cac","analyzedAt":"2026-08-17T05:17:23.470Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}