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

  1. Inspect what is actually stored: var_dump(strlen($hash), substr($hash, 0, 7)); a Yii2-generated hash starts with $2y$10$.
  2. 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.
  3. For legacy md5/sha1 hashes, verify with the legacy algorithm, then transparently rehash to bcrypt on successful login (rehash-on-login migration).
  4. 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

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


AI-assisted analysis of yiisoft/yii2@66f00d18a2 (2026-08-17). Data as JSON: /api/errors/f44c40cc77b90a8d. Report an issue: GitHub.