{"record":{"id":"a0a4b7597ec73a59","repo":"yiisoft/yii2","slug":"expected-expected-value-to-be-a-string","errorCode":null,"errorMessage":"Expected expected value to be a string, ","messagePattern":"Expected expected value to be a string, ","errorType":"exception","errorClass":"InvalidArgumentException","httpStatus":null,"severity":"error","filePath":"framework/base/Security.php","lineNumber":547,"sourceCode":"        // Form the prefix that specifies Blowfish (bcrypt) algorithm and cost parameter.\n        $salt = sprintf('$2y$%02d$', $cost);\n        // Append the random salt data in the required base64 format.\n        $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22));\n\n        return $salt;\n    }\n\n    /**\n     * Performs string comparison using timing attack resistant approach.\n     * @see https://codereview.stackexchange.com/q/13512\n     * @param string $expected string to compare.\n     * @param string $actual user-supplied string.\n     * @return bool whether strings are equal.\n     */\n    public function compareString($expected, $actual)\n    {\n        if (!is_string($expected)) {\n            throw new InvalidArgumentException('Expected expected value to be a string, ' . gettype($expected) . ' given.');\n        }\n\n        if (!is_string($actual)) {\n            throw new InvalidArgumentException('Expected actual value to be a string, ' . gettype($actual) . ' given.');\n        }\n\n        return hash_equals($expected, $actual);\n    }\n\n    /**\n     * Masks a token to make it uncompressible.\n     * Applies a random mask to the token and prepends the mask used to the result making the string always unique.\n     * Used to mitigate BREACH attack by randomizing how token is outputted on each request.\n     * @param string $token An unmasked token.\n     * @return string A masked token.\n     * @since 2.0.12\n     */\n    public function maskToken($token)","sourceCodeStart":529,"sourceCodeEnd":565,"githubUrl":"https://github.com/yiisoft/yii2/blob/66f00d18a29b520f85e8e8f1e32d1e7e7b556cac/framework/base/Security.php#L529-L565","documentation":"Thrown by yii\\base\\Security::compareString() when the $expected argument (the trusted/reference side of a timing-attack-safe comparison) is not a PHP string. The method wraps hash_equals(), which requires strings, so it guards both arguments with is_string() first. The message appends gettype() of the offending value, e.g. 'Expected expected value to be a string, NULL given.'","triggerScenarios":"Calling compareString(null, $token) when a secret read from config with a wrong key returns null; comparing against an empty DB column (NULL api_secret, deleted cookie validation key); passing an int/float constant or an array/object as $expected; a json_decode() or unserialize() result used directly as the reference value.","commonSituations":"API-key or HMAC verification where the stored secret is null for legacy rows; cookie/security key missing from the production config; fixtures or seeds that leave token columns null; refactoring a constant to a config value that is not actually defined in that environment.","solutions":["Read the gettype() named in the message and trace where that $expected value comes from.","Fix the source: correct the config key path, add a NOT NULL default to the column, or regenerate/seed the missing secret.","If absence is legitimate, short-circuit before calling: treat a non-string expected value as an automatic mismatch instead of an exception.","For nullable values use is_string($expected) && Yii::$app->security->compareString($expected, $actual)."],"exampleFix":"// before\n$valid = Yii::$app->security->compareString($user->api_secret, $requestSecret);\n// $user->api_secret is NULL for rows created before the column existed\n\n// after\n$valid = is_string($user->api_secret)\n    && Yii::$app->security->compareString($user->api_secret, (string) $requestSecret);","handlingStrategy":"type-guard","validationCode":"if (!is_string($expected)) {\n    // regenerate/refresh the secret, or treat as an automatic mismatch\n    return false;\n}\nreturn Yii::$app->security->compareString($expected, $actual);","typeGuard":"function assertComparableSecret($value): string\n{\n    if (!is_string($value)) {\n        throw new \\InvalidArgumentException(\n            'Expected secret to be a string, ' . gettype($value) . ' given.'\n        );\n    }\n    return $value;\n}","tryCatchPattern":null,"preventionTips":["Never feed nullable lookups straight into compareString(); check is_string() first","Define secrets as NOT NULL with sane defaults so the reference side is always a string","Fail loudly at boot if required security keys are missing from config, instead of null at compare time"],"tags":["php","yii2","security","string-comparison","timing-attack","null-safety"],"backgroundTag":"wrong-argument-type","analyzedSha":"66f00d18a29b520f85e8e8f1e32d1e7e7b556cac","analyzedAt":"2026-08-17T05:17:23.470Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}