CVE-2023-27580 (published 2023-03-13T18:15Z)

CodeIgniter Shield provides authentication and authorization for the CodeIgniter 4 PHP framework. An improper implementation was found in the password storage process. All hashed passwords stored in Shield v1.0.0-beta.3 or earlier are easier to crack than expected due to the vulnerability. Therefore, they should be removed as soon as possible. If an attacker gets (1) the user's hashed password by Shield, and (2) the hashed password (SHA-384 hash without salt) from somewhere, the attacker may easily crack the user's password. Upgrade to Shield v1.0.0-beta.4 or later to fix this issue. After upgrading, all users’ hashed passwords should be updated (saved to the database). There are no known workarounds.

CVSS v3 base score: 5.9, MEDIUM (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-916
cwe_name: Use of Password Hash With Insufficient Computational Effort
description: The product generates a hash for a password, but it uses a scheme that does not provide a sufficient level of computational effort that would make password cracking attacks infeasible or expensive.
extended_description: 
            Many password storage mechanisms compute a hash and store the hash, instead of storing the original password in plaintext. In this design, authentication involves accepting an incoming password, computing its hash, and comparing it to the stored hash.
            Many hash algorithms are designed to execute quickly with minimal overhead, even cryptographic hashes. However, this efficiency is a problem for password storage, because it can reduce an attacker's workload for brute-force password cracking. If an attacker can obtain the hashes through some other method (such as SQL injection on a database that stores hashes), then the attacker can store the hashes offline and use various techniques to crack the passwords by computing hashes efficiently. Without a built-in workload, modern attacks can compute large numbers of hashes, or even exhaust the entire space of all possible passwords, within a very short amount of time, using massively-parallel computing (such as cloud computing) and GPU, ASIC, or FPGA hardware. In such a scenario, an efficient hash algorithm helps the attacker.
            There are several properties of a hash scheme that are relevant to its strength against an offline, massively-parallel attack:
               
                  The amount of CPU time required to compute the hash ("stretching")
                  The amount of memory required to compute the hash ("memory-hard" operations)
                  Including a random value, along with the password, as input to the hash computation ("salting")
                  Given a hash, there is no known way of determining an input (e.g., a password) that produces this hash value, other than by guessing possible inputs ("one-way" hashing)
                  Relative to the number of all possible hashes that can be generated by the scheme, there is a low likelihood of producing the same hash for multiple different inputs ("collision resistance")
               
            Note that the security requirements for the product may vary depending on the environment and the value of the passwords. Different schemes might not provide all of these properties, yet may still provide sufficient security for the environment. Conversely, a solution might be very strong in preserving one property, which still being very weak for an attack against another property, or it might not be able to significantly reduce the efficiency of a massively-parallel attack.
         
         

Fix information:
repo_url: https://github.com/codeigniter4/shield
commit_details: repo_url: https://github.com/codeigniter4/shield
author: kenjis
author_date: 2023-03-11 14:01:18+09:00
msg: Merge pull request from GHSA-c5vj-f36q-p9vg

* feat: copy the current methods as depreacted

* fix: password shucking

* feat: support old dangerous passwords

* docs: add Upgrade Guide

* refactor: fix variable name typo

Co-authored-by: MGatner <mgatner@icloud.com>

* refactor: fix variable name typo

* fix: $options is missing for password_needs_rehash()

* docs: add OWASP Cheat Sheet in README

* docs: add explanation in UPGRADING.md

* docs: replace Config/Auth.php with app/Config/Auth.php

* docs: add How to Strengthen the Password

* docs: UPGRADING.md

* docs: add note in "Minimum Password Length"

* docs: $supportOldDangerousPassword will be removed in v1.0.0

* feat: add validation rule for max password length

PASSWORD_BCRYPT -> 72 bytes
Others -> 255 characters

* docs: update docs

* feat: add new lang item to new lang files

* feat: add errorPasswordTooLongBytes to Language/pt-BR

* refactor: ChangeIfElseValueAssignToEarlyReturnRector

---------

Co-authored-by: MGatner <mgatner@icloud.com>
repository: repo_url: https://github.com/codeigniter4/shield
repo_name: codeigniter4/shield
description: Authentication and Authorization for CodeIgniter 4
repo_language: PHP
owner: codeigniter4
file_changes: filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -86,3 +86,4 @@ within this library, in no particular order:
 - [Google Cloud: 13 best practices for user account, authentication, and password management, 2021 edition](https://cloud.google.com/blog/products/identity-security/account-authentication-and-password-management-best-practices)
 - [NIST Digital Identity Guidelines](https://pages.nist.gov/800-63-3/sp800-63b.html)
 - [Implementing Secure User Authentication in PHP Applications with Long-Term Persistence (Login with "Remember Me" Cookies) ](https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence)
+- [Password Storage - OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)

programming_language: Markdown
filename: UPGRADING.md
change_type: ModificationType.ADD
diff: @@ -0,0 +1,51 @@
+# Upgrade Guide
+
+## Version 1.0.0-beta.3 to 1.0.0-beta.4
+
+### Important Password Changes
+
+#### Password Incompatibility
+
+Shield 1.0.0-beta.4 fixes a [vulnerability related to password storage](https://github.com/codeigniter4/shield/security/advisories/GHSA-c5vj-f36q-p9vg).
+As a result, hashed passwords already stored in the database are no longer compatible
+and cannot be used by default.
+
+All hashed passwords stored in Shield v1.0.0-beta.3 or earlier are easier to
+crack than expected due to the above vulnerability. Therefore, they should be
+removed as soon as possible.
+
+Existing users will no longer be able to log in with their passwords and will
+need to log in with the magic link and then set their passwords again.
+
+#### If You Want to Allow Login with Existing Passwords
+
+If you want to use passwords saved in Shield v1.0.0-beta.3 or earlier,
+you must add the following property in `app/Config/Auth.php`:
+
+```php
+    public bool $supportOldDangerousPassword = true;
+```
+
+After upgrading, with the above setting, once a user logs in with the password,
+the hashed password is updated and stored in the database.
+
+In this case, the existing hashed passwords are still easier to crack than expected.
+Therefore, this setting should not be used for an extended period of time.
+So you should change the setting to `false` as soon as possible, and remove old
+hashed password.
+
+> **Note**
+>
+> This setting is deprecated. It will be removed in v1.0.0 official release.
+
+#### Limitations for the Default Password Handling
+
+By default, Shield uses the hashing algorithm `PASSWORD_DEFAULT` (see `app/Config/Auth.php`),
+that is, `PASSWORD_BCRYPT` at the time of writing.
+
+Now there are two limitations when you use `PASSWORD_BCRYPT`.
+
+1. the password will be truncated to a maximum length of 72 bytes.
+2. the password will be truncated at the first NULL byte (`\0`).
+
+If these behaviors are unacceptable, see [How to Strengthen the Password](https://github.com/codeigniter4/shield/blob/develop/docs/guides/strengthen_password.md).

programming_language: Markdown
filename: strengthen_password.md
change_type: ModificationType.ADD
diff: @@ -0,0 +1,122 @@
+# How to Strengthen the Password
+
+Shield allows you to customize password-related settings to make your passwords more secure.
+
+## Minimum Password Length
+
+The most important factor when it comes to passwords is the number of characters in the password.
+You can check password strength with [Password Strength Testing Tool](https://bitwarden.com/password-strength/).
+Short passwords may be cracked in less than one day.
+
+In Shield, you can set the users' minimum password length. The setting is
+`$minimumPasswordLength` in `app/Config/Auth.php`. The default value is 8 characters.
+It is the recommended minimum value by NIST. However, some organizations recommend
+12 to 14 characters.
+
+The longer the password, the stronger it is. Consider increasing the value.
+
+> **Note**
+>
+> This checking works when you validate passwords with the `strong_password`
+> validation rule.
+>
+> If you disable `CompositionValidator` (enabled by default) in `$passwordValidators`,
+> this checking will not work.
+
+## Password Hashing Algorithm
+
+You can change the password hashing algorithm by `$hashAlgorithm` in `app/Config/Auth.php`.
+The default value is `PASSWORD_DEFAULT` that is `PASSWORD_BCRYPT` at the time of writing.
+
+`PASSWORD_BCRYPT` means to create new password hashes using the bcrypt algorithm.
+
+You can use `PASSWORD_ARGON2ID` if your PHP has been compiled with Argon2 support.
+
+### PASSWORD_BCRYPT
+
+`PASSWORD_BCRYPT` has one configuration `$hashCost`. The bigger the cost, hashed passwords will be the stronger.
+
+You can find your appropriate cost with the following code:
+
+```php
+<?php
+/**
+ * This code will benchmark your server to determine how high of a cost you can
+ * afford. You want to set the highest cost that you can without slowing down
+ * you server too much. 8-10 is a good baseline, and more is good if your servers
+ * are fast enough. The code below aims for ≤ 50 milliseconds stretching time,
+ * which is a good baseline for systems handling interactive logins.
+ *
+ * From: https://www.php.net/manual/en/function.password-hash.php#refsect1-function.password-hash-examples
+ */
+$timeTarget = 0.05; // 50 milliseconds
+
+$cost = 8;
+do {
+    $cost++;
+    $start = microtime(true);
+    password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
+    $end = microtime(true);
+} while (($end - $start) < $timeTarget);
+
+echo "Appropriate Cost Found: " . $cost;
+```
+
+#### Limitations
+
+There are two limitations when you use `PASSWORD_BCRYPT`:
+
+1. the password will be truncated to a maximum length of 72 bytes.
+2. the password will be truncated at the first NULL byte (`\0`).
+
+##### 72 byte issue
+
+If a user submits a password longer than 72 bytes, the validation error will occur.
+If this behavior is unacceptable, consider:
+
+1. change the hashing algorithm to `PASSWORD_ARGON2ID`. It does not have such a limitation.
+
+##### NULL byte issue
+
+This is because `PASSWORD_BCRYPT` is not binary-safe. Normal users cannot
+send NULL bytes in a password string, so this is not a problem in most cases.
+
+But if this behavior is unacceptable, consider:
+
+1. adding a validation rule to prohibit NULL bytes or control codes.
+2. or change the hashing algorithm to `PASSWORD_ARGON2ID`. It is binary-safe.
+
+### PASSWORD_ARGON2ID
+
+`PASSWORD_ARGON2ID` has three configuration `$hashMemoryCost`, `$hashTimeCost`,
+and `$hashThreads`.
+
+If you use `PASSWORD_ARGON2ID`, you should use PHP's constants:
+
+```php
+    public int $hashMemoryCost = PASSWORD_ARGON2_DEFAULT_MEMORY_COST;
+
+    public int $hashTimeCost = PASSWORD_ARGON2_DEFAULT_TIME_COST;
+    public int $hashThreads  = PASSWORD_ARGON2_DEFAULT_THREADS;
+```
+
+## Maximum Password Length
+
+By default, Shield has the validation rules for maximum password length.
+
+- 72 bytes for PASSWORD_BCRYPT
+- 255 characters for others
+
+You can customize the validation rule. See [Customizing Shield](../customization.md).
+
+## $supportOldDangerousPassword
+
+In `app/Config/Auth.php` there is `$supportOldDangerousPassword`, which is a
+setting for using passwords stored in older versions of Shield that were [vulnerable](https://github.com/codeigniter4/shield/security/advisories/GHSA-c5vj-f36q-p9vg).
+
+This setting is deprecated.  If you have this setting set to `true`, you should change
+it to `false` as soon as possible, and remove old hashed password in your database.
+
+> **Note**
+>
+> This setting will be removed in v1.0.0 official release.

programming_language: Markdown
filename: index.md
change_type: ModificationType.MODIFY
diff: @@ -14,3 +14,4 @@
 ## Guides
 * [Protecting an API with Access Tokens](guides/api_tokens.md)
 * [Mobile Authentication with Access Tokens](guides/mobile_apps.md)
+* [How to Strengthen the Password](guides/strengthen_password.md)

programming_language: Markdown
filename: mkdocs.yml
change_type: ModificationType.MODIFY
diff: @@ -51,3 +51,4 @@ nav:
   - Guides:
     - guides/api_tokens.md
     - guides/mobile_apps.md
+    - guides/strengthen_password.md

programming_language: Markdown
filename: Session.php
change_type: ModificationType.MODIFY
diff: @@ -336,19 +336,30 @@ public function check(array $credentials): Result
         /** @var Passwords $passwords */
         $passwords = service('passwords');
 
+        // This is only for supportOldDangerousPassword.
+        $needsRehash = false;
+
         // Now, try matching the passwords.
         if (! $passwords->verify($givenPassword, $user->password_hash)) {
-            return new Result([
-                'success' => false,
-                'reason'  => lang('Auth.invalidPassword'),
-            ]);
+            if (
+                ! setting('Auth.supportOldDangerousPassword')
+                || ! $passwords->verifyDanger($givenPassword, $user->password_hash) // @phpstan-ignore-line
+            ) {
+                return new Result([
+                    'success' => false,
+                    'reason'  => lang('Auth.invalidPassword'),
+                ]);
+            }
+
+            // Passed with old dangerous password.
+            $needsRehash = true;
         }
 
         // Check to see if the password needs to be rehashed.
         // This would be due to the hash algorithm or hash
         // cost changing since the last time that a user
         // logged in.
-        if ($passwords->needsRehash($user->password_hash)) {
+        if ($passwords->needsRehash($user->password_hash) || $needsRehash) {
             $user->password_hash = $passwords->hash($givenPassword);
             $this->provider->save($user);
         }

programming_language: PHP
filename: Passwords.php
change_type: ModificationType.MODIFY
diff: @@ -31,26 +31,42 @@ public function __construct(Auth $config)
      */
     public function hash(string $password)
     {
-        if ((defined('PASSWORD_ARGON2I') && $this->config->hashAlgorithm === PASSWORD_ARGON2I)
+        return password_hash($password, $this->config->hashAlgorithm, $this->getHashOptions());
+    }
+
+    private function getHashOptions(): array
+    {
+        if (
+            (defined('PASSWORD_ARGON2I') && $this->config->hashAlgorithm === PASSWORD_ARGON2I)
             || (defined('PASSWORD_ARGON2ID') && $this->config->hashAlgorithm === PASSWORD_ARGON2ID)
         ) {
-            $hashOptions = [
+            return [
                 'memory_cost' => $this->config->hashMemoryCost,
                 'time_cost'   => $this->config->hashTimeCost,
                 'threads'     => $this->config->hashThreads,
             ];
-        } else {
-            $hashOptions = [
-                'cost' => $this->config->hashCost,
-            ];
         }
 
+        return [
+            'cost' => $this->config->hashCost,
+        ];
+    }
+
+    /**
+     * Hash a password.
+     *
+     * @return false|string|null
+     *
+     * @deprecated This is only for backward compatibility.
+     */
+    public function hashDanger(string $password)
+    {
         return password_hash(
             base64_encode(
                 hash('sha384', $password, true)
             ),
             $this->config->hashAlgorithm,
-            $hashOptions
+            $this->getHashOptions()
         );
     }
 
@@ -61,6 +77,19 @@ public function hash(string $password)
      * @param string $hash     The previously hashed password
      */
     public function verify(string $password, string $hash): bool
+    {
+        return password_verify($password, $hash);
+    }
+
+    /**
+     * Verifies a password against a previously hashed password.
+     *
+     * @param string $password The password we're checking
+     * @param string $hash     The previously hashed password
+     *
+     * @deprecated This is only for backward compatibility.
+     */
+    public function verifyDanger(string $password, string $hash): bool
     {
         return password_verify(base64_encode(
             hash('sha384', $password, true)
@@ -72,7 +101,7 @@ public function verify(string $password, string $hash): bool
      */
     public function needsRehash(string $hashedPassword): bool
     {
-        return password_needs_rehash($hashedPassword, $this->config->hashAlgorithm);
+        return password_needs_rehash($hashedPassword, $this->config->hashAlgorithm, $this->getHashOptions());
     }
 
     /**
@@ -110,4 +139,16 @@ public function check(string $password, ?User $user = null): Result
             'success' => true,
         ]);
     }
+
+    /**
+     * Returns the validation rule for max length.
+     */
+    public static function getMaxLenghtRule(): string
+    {
+        if (config('Auth')->hashAlgorithm === PASSWORD_BCRYPT) {
+            return 'max_byte[72]';
+        }
+
+        return 'max_length[255]';
+    }
 }

programming_language: PHP
filename: ValidationRules.php
change_type: ModificationType.MODIFY
diff: @@ -55,6 +55,14 @@ public function strong_password(string $value, ?string &$error1 = null, array $d
         return $result->isOk();
     }
 
+    /**
+     * Returns true if $str is $val or fewer bytes in length.
+     */
+    public function max_byte(?string $str, string $val): bool
+    {
+        return is_numeric($val) && $val >= strlen($str ?? '');
+    }
+
     /**
      * Builds a new user instance from the global request.
      */

programming_language: PHP
filename: Auth.php
change_type: ModificationType.MODIFY
diff: @@ -360,6 +360,16 @@ class Auth extends BaseConfig
      */
     public int $hashCost = 10;
 
+    /**
+     * If you need to support passwords saved in versions prior to Shield v1.0.0-beta.4.
+     * set this to true.
+     *
+     * See https://github.com/codeigniter4/shield/security/advisories/GHSA-c5vj-f36q-p9vg
+     *
+     * @deprecated This is only for backward compatibility.
+     */
+    public bool $supportOldDangerousPassword = false;
+
     /**
      * ////////////////////////////////////////////////////////////////////
      * OTHER SETTINGS

programming_language: PHP
filename: LoginController.php
change_type: ModificationType.MODIFY
diff: @@ -7,6 +7,7 @@
 use App\Controllers\BaseController;
 use CodeIgniter\HTTP\RedirectResponse;
 use CodeIgniter\Shield\Authentication\Authenticators\Session;
+use CodeIgniter\Shield\Authentication\Passwords;
 use CodeIgniter\Shield\Traits\Viewable;
 
 class LoginController extends BaseController
@@ -90,8 +91,11 @@ protected function getValidationRules(): array
                 'rules' => config('AuthSession')->emailValidationRules,
             ],
             'password' => [
-                'label' => 'Auth.password',
-                'rules' => 'required',
+                'label'  => 'Auth.password',
+                'rules'  => 'required|' . Passwords::getMaxLenghtRule(),
+                'errors' => [
+                    'max_byte' => 'Auth.errorPasswordTooLongBytes',
+                ],
             ],
         ];
     }

programming_language: PHP
filename: RegisterController.php
change_type: ModificationType.MODIFY
diff: @@ -10,6 +10,7 @@
 use CodeIgniter\HTTP\RequestInterface;
 use CodeIgniter\HTTP\ResponseInterface;
 use CodeIgniter\Shield\Authentication\Authenticators\Session;
+use CodeIgniter\Shield\Authentication\Passwords;
 use CodeIgniter\Shield\Config\Auth;
 use CodeIgniter\Shield\Entities\User;
 use CodeIgniter\Shield\Exceptions\ValidationException;
@@ -195,8 +196,11 @@ protected function getValidationRules(): array
                 'rules' => $registrationEmailRules,
             ],
             'password' => [
-                'label' => 'Auth.password',
-                'rules' => 'required|strong_password',
+                'label'  => 'Auth.password',
+                'rules'  => 'required|' . Passwords::getMaxLenghtRule() . '|strong_password',
+                'errors' => [
+                    'max_byte' => 'Auth.errorPasswordTooLongBytes',
+                ],
             ],
             'password_confirm' => [
                 'label' => 'Auth.passwordConfirm',

programming_language: PHP
filename: Auth.php
change_type: ModificationType.MODIFY
diff: @@ -61,6 +61,7 @@
     'errorPasswordPwned'        => 'Das Passwort {0} wurde aufgrund einer Datenschutzverletzung aufgedeckt und wurde {1, number} Mal in {2} kompromittierten Passwörtern gesehen.',
     'suggestPasswordPwned'      => '{0} sollte niemals als Passwort verwendet werden. Wenn Sie es irgendwo verwenden, ändern Sie es sofort.',
     'errorPasswordEmpty'        => 'Ein Passwort ist erforderlich.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Passwort erfolgreich geändert',
     'userDoesNotExist'          => 'Passwort wurde nicht geändert. Der Benutzer existiert nicht',
     'resetTokenExpired'         => 'Tut mir leid. Ihr Reset-Token ist abgelaufen.',

programming_language: PHP
filename: Auth.php
change_type: ModificationType.MODIFY
diff: @@ -61,6 +61,7 @@
     'errorPasswordPwned'        => 'The password {0} has been exposed due to a data breach and has been seen {1, number} times in {2} of compromised passwords.',
     'suggestPasswordPwned'      => '{0} should never be used as a password. If you are using it anywhere change it immediately.',
     'errorPasswordEmpty'        => 'A Password is required.',
+    'errorPasswordTooLongBytes' => 'Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Password changed successfully',
     'userDoesNotExist'          => 'Password was not changed. User does not exist',
     'resetTokenExpired'         => 'Sorry. Your reset token has expired.',

programming_language: PHP
filename: Auth.php
change_type: ModificationType.MODIFY
diff: @@ -61,6 +61,7 @@
     'errorPasswordPwned'        => 'La contraseña {0} ha quedado expuesta debido a una violación de datos y se ha visto comprometida {1, número} veces en {2} contraseñas.',
     'suggestPasswordPwned'      => '{0} no se debe usar nunca como contraseña. Si la estás usando en algún sitio, cámbiala inmediatamente.',
     'errorPasswordEmpty'        => 'Se necesita una contraseña.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Contraseña modificada correctamente',
     'userDoesNotExist'          => 'No se ha cambiado la contraseña. No existe el usuario',
     'resetTokenExpired'         => 'Lo sentimos. Tu token de reseteo ha caducado.',

programming_language: PHP
filename: Auth.php
change_type: ModificationType.MODIFY
diff: @@ -61,6 +61,7 @@
     'errorPasswordPwned'        => 'رمز عبور {0} با توجه به نقض داده ها و دیده شدن {1, number} بارها داخل رمز های عبور {2} به پسورد های ناامن تبدیل شده و در معرض قرار گرفته است.',
     'suggestPasswordPwned'      => '{0} هرگز نباید به عنوان رمز عبور استفاده شود. اگر در هر جایی از آن استفاده می کنید سریعا آن را تغییر دهید.',
     'errorPasswordEmpty'        => 'رمز عبور الزامی است.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'رمز عبور با موفقیت تغییر کرد',
     'userDoesNotExist'          => 'رمز عبور تغییر نکرد. کاربر وجود ندارد.',
     'resetTokenExpired'         => 'متاسفانه، توکن بازنشانی شما منقضی شده است.',

programming_language: PHP
filename: Auth.php
change_type: ModificationType.MODIFY
diff: @@ -61,6 +61,7 @@
     'errorPasswordPwned'        => 'Le mot de passe {0} a été exposé à la suite d\'une violation de données et a été vu {1, number} fois dans {2} des mots de passe compromis.',
     'suggestPasswordPwned'      => '{0} ne devrait jamais être utilisé comme mot de passe. Si vous l\'utilisez quelque part, changez-le immédiatement.',
     'errorPasswordEmpty'        => 'Un mot de passe est obligatoire.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Mot de passe modifié avec succès',
     'userDoesNotExist'          => 'Le mot de passe n\'a pas été modifié. L\'utilisateur n\'existe pas',
     'resetTokenExpired'         => 'Désolé. Votre jeton de réinitialisation a expiré.',

programming_language: PHP
filename: Auth.php
change_type: ModificationType.MODIFY
diff: @@ -61,6 +61,7 @@
     'errorPasswordPwned'        => 'Kata sandi {0} telah bocor karena pelanggaran data dan telah dilihat {1, number} kali dalam {2} sandi yang disusupi.',
     'suggestPasswordPwned'      => '{0} tidak boleh digunakan sebagai kata sandi. Jika Anda menggunakannya di mana saja, segera ubah.',
     'errorPasswordEmpty'        => 'Kata sandi wajib diisi.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Kata sandi berhasil diubah',
     'userDoesNotExist'          => 'Kata sandi tidak diubah. User tidak ditemukan',
     'resetTokenExpired'         => 'Maaf, token setel ulang Anda sudah habis waktu.',

programming_language: PHP
filename: Auth.php
change_type: ModificationType.MODIFY
diff: @@ -61,6 +61,7 @@
     'errorPasswordPwned'        => 'La password {0} è stata esposta ad un furto di dati ed è stata vista {1, number} volte in {2} di password compromesse.',
     'suggestPasswordPwned'      => '{0} non dovrebbe mai essere usata come password. Se la stai utilizzando da qualche parte, cambiala immediatamente.',
     'errorPasswordEmpty'        => 'Una password è richiesta.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'La password è stata cambiata con successo',
     'userDoesNotExist'          => 'La password non è stata cambiata. L\'utente non esiste',
     'resetTokenExpired'         => 'Spiacente. Il tuo reset token è scaduto.',

programming_language: PHP
filename: Auth.php
change_type: ModificationType.MODIFY
diff: @@ -61,6 +61,7 @@
     'errorPasswordPwned'        => 'パスワード {0} はデータ漏洩により公開されており、{2} の漏洩したパスワード中で {1, number} 回見られます。', // 'The password {0} has been exposed due to a data breach and has been seen {1, number} times in {2} of compromised passwords.',
     'suggestPasswordPwned'      => '{0} は絶対にパスワードとして使ってはいけません。もしどこかで使っていたら、すぐに変更してください。', // '{0} should never be used as a password. If you are using it anywhere change it immediately.',
     'errorPasswordEmpty'        => 'パスワードが必要です。', // 'A Password is required.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'パスワードの変更に成功しました', // 'Password changed successfully',
     'userDoesNotExist'          => 'パスワードは変更されていません。ユーザーは存在しません', // 'Password was not changed. User does not exist',
     'resetTokenExpired'         => '申し訳ありません。リセットトークンの有効期限が切れました。', // 'Sorry. Your reset token has expired.',

programming_language: PHP
filename: Auth.php
change_type: ModificationType.MODIFY
diff: @@ -61,6 +61,7 @@
     'errorPasswordPwned'        => 'A senha {0} foi exposta devido a uma violação de dados e foi vista {1, number} vezes em {2} de senhas comprometidas.',
     'suggestPasswordPwned'      => '{0} nunca deve ser usado como uma senha. Se você estiver usando em algum lugar, altere imediatamente.',
     'errorPasswordEmpty'        => 'É necessária uma senha.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Senha alterada com sucesso',
     'userDoesNotExist'          => 'Senha não foi alterada. Usuário não existe',
     'resetTokenExpired'         => 'Desculpe. Seu token de redefinição expirou.',

programming_language: PHP
filename: Auth.php
change_type: ModificationType.MODIFY
diff: @@ -61,6 +61,7 @@
     'errorPasswordPwned'        => 'Heslo {0} bolo odhalené z dôvodu porušenia ochrany údajov a bolo videné {1, number}-krát z {2} prelomených hesiel.',
     'suggestPasswordPwned'      => '{0} by sa nikdy nemalo používať ako heslo. Ak ho niekde používate, okamžite ho zmeňte.',
     'errorPasswordEmpty'        => 'Vyžaduje sa heslo.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Heslo bolo úspešne zmenené',
     'userDoesNotExist'          => 'Heslo nebolo zmenené. Používateľ neexistuje',
     'resetTokenExpired'         => 'Prepáčte. Platnosť vášho resetovacieho tokenu vypršala.',

programming_language: PHP
filename: Auth.php
change_type: ModificationType.MODIFY
diff: @@ -61,6 +61,7 @@
     'errorPasswordPwned'        => '{0} şifresi, bir veri ihlali nedeniyle açığa çıktı ve güvenliği ihlal edilmiş şifrelerin {2} tanesinde {1, sayı} kez görüldü.',
     'suggestPasswordPwned'      => '{0} asla şifre olarak kullanılmamalıdır. Herhangi bir yerde kullanıyorsanız hemen değiştirin.',
     'errorPasswordEmpty'        => 'Şifre gerekli.',
+    'errorPasswordTooLongBytes' => '(To be translated) Password cannot exceed {param} bytes in length.',
     'passwordChangeSuccess'     => 'Şifre başarıyla değiştirildi.',
     'userDoesNotExist'          => 'Şifre değiştirilmedi. Kullanıcı yok.',
     'resetTokenExpired'         => 'Üzgünüz. Sıfırlama anahtarınızın süresi doldu.',

programming_language: PHP
filename: SessionAuthenticatorTest.php
change_type: ModificationType.MODIFY
diff: @@ -12,6 +12,7 @@
 use CodeIgniter\Shield\Entities\User;
 use CodeIgniter\Shield\Exceptions\LogicException;
 use CodeIgniter\Shield\Models\RememberModel;
+use CodeIgniter\Shield\Models\UserIdentityModel;
 use CodeIgniter\Shield\Models\UserModel;
 use CodeIgniter\Shield\Result;
 use CodeIgniter\Test\Mock\MockEvents;
@@ -303,6 +304,34 @@ public function testCheckSuccess(): void
         $this->assertSame($this->user->id, $foundUser->id);
     }
 
+    public function testCheckSuccessOldDangerousPassword(): void
+    {
+        /** @var Auth $config */
+        $config                              = config('Auth');
+        $config->supportOldDangerousPassword = true; // @phpstan-ignore-line
+
+        fake(
+            UserIdentityModel::class,
+            [
+                'user_id' => $this->user->id,
+                'type'    => Session::ID_TYPE_EMAIL_PASSWORD,
+                'secret'  => 'foo@example.com',
+                'secret2' => '$2y$10$WswjNNcR24cJvsXvBc5TveVVVQ9/EYC0eq.Ad9e/2cVnmeSEYBOEm',
+            ]
+        );
+
+        $result = $this->auth->check([
+            'email'    => 'foo@example.com',
+            'password' => 'passw0rd!',
+        ]);
+
+        $this->assertInstanceOf(Result::class, $result);
+        $this->assertTrue($result->isOK());
+
+        $foundUser = $result->extraInfo();
+        $this->assertSame($this->user->id, $foundUser->id);
+    }
+
     public function testAttemptCannotFindUser(): void
     {
         $result = $this->auth->attempt([

programming_language: PHP
filename: LoginTest.php
change_type: ModificationType.MODIFY
diff: @@ -8,6 +8,7 @@
 use CodeIgniter\Config\Factories;
 use CodeIgniter\I18n\Time;
 use CodeIgniter\Shield\Authentication\Actions\Email2FA;
+use CodeIgniter\Shield\Config\Auth;
 use CodeIgniter\Test\FeatureTestTrait;
 use Config\Services;
 use Config\Validation;
@@ -61,6 +62,52 @@ public function testLoginBadEmail(): void
         $this->assertSame(lang('Auth.badAttempt'), session('error'));
     }
 
+    public function testLoginTooLongPasswordDefault(): void
+    {
+        $this->user->createEmailIdentity([
+            'email'    => 'foo@example.com',
+            'password' => 'secret123',
+        ]);
+
+        $result = $this->post('/login', [
+            'email'    => 'fooled@example.com',
+            'password' => str_repeat('a', 73),
+        ]);
+
+        $result->assertStatus(302);
+        $result->assertRedirect();
+        $result->assertSessionMissing('error');
+        $result->assertSessionHas(
+            'errors',
+            ['password' => 'Password cannot exceed 72 bytes in length.']
+        );
+    }
+
+    public function testLoginTooLongPasswordArgon2id(): void
+    {
+        /** @var Auth $config */
+        $config                = config('Auth');
+        $config->hashAlgorithm = PASSWORD_ARGON2ID;
+
+        $this->user->createEmailIdentity([
+            'email'    => 'foo@example.com',
+            'password' => 'secret123',
+        ]);
+
+        $result = $this->post('/login', [
+            'email'    => 'fooled@example.com',
+            'password' => str_repeat('a', 256),
+        ]);
+
+        $result->assertStatus(302);
+        $result->assertRedirect();
+        $result->assertSessionMissing('error');
+        $result->assertSessionHas(
+            'errors',
+            ['password' => 'The Password field cannot exceed 255 characters in length.']
+        );
+    }
+
     public function testLoginActionEmailSuccess(): void
     {
         if (version_compare(CodeIgniter::CI_VERSION, '4.3.0', '>=')) {

programming_language: PHP
filename: RegisterTest.php
change_type: ModificationType.MODIFY
diff: @@ -8,6 +8,7 @@
 use CodeIgniter\Shield\Authentication\Actions\EmailActivator;
 use CodeIgniter\Shield\Authentication\Authenticators\Session;
 use CodeIgniter\Shield\Authentication\Passwords\ValidationRules;
+use CodeIgniter\Shield\Config\Auth;
 use CodeIgniter\Shield\Entities\User;
 use CodeIgniter\Shield\Models\UserModel;
 use CodeIgniter\Test\FeatureTestTrait;
@@ -81,6 +82,46 @@ public function testRegisterActionSuccess(): void
         $this->assertTrue($user->active);
     }
 
+    public function testRegisterTooLongPasswordDefault(): void
+    {
+        $result = $this->withSession()->post('/register', [
+            'username'         => 'JohnDoe',
+            'email'            => 'john.doe@example.com',
+            'password'         => str_repeat('a', 73),
+            'password_confirm' => str_repeat('a', 73),
+        ]);
+
+        $result->assertStatus(302);
+        $result->assertRedirect();
+        $result->assertSessionMissing('error');
+        $result->assertSessionHas(
+            'errors',
+            ['password' => 'Password cannot exceed 72 bytes in length.']
+        );
+    }
+
+    public function testRegisterTooLongPasswordArgon2id(): void
+    {
+        /** @var Auth $config */
+        $config                = config('Auth');
+        $config->hashAlgorithm = PASSWORD_ARGON2ID;
+
+        $result = $this->withSession()->post('/register', [
+            'username'         => 'JohnDoe',
+            'email'            => 'john.doe@example.com',
+            'password'         => str_repeat('a', 256),
+            'password_confirm' => str_repeat('a', 256),
+        ]);
+
+        $result->assertStatus(302);
+        $result->assertRedirect();
+        $result->assertSessionMissing('error');
+        $result->assertSessionHas(
+            'errors',
+            ['password' => 'The Password field cannot exceed 255 characters in length.']
+        );
+    }
+
     public function testRegisterDisplaysForm(): void
     {
         $result = $this->withSession()->get('/register');

programming_language: PHP
filename: PasswordsTest.php
change_type: ModificationType.MODIFY
diff: @@ -23,4 +23,40 @@ public function testEmptyPassword(): void
         $this->assertFalse($result->isOK());
         $this->assertSame('A Password is required.', $result->reason());
     }
+
+    public function testHash(): string
+    {
+        $config    = new AuthConfig();
+        $passwords = new Passwords($config);
+
+        $plainPassword  = 'passw0rd!';
+        $hashedPassword = $passwords->hash($plainPassword);
+
+        $user = new User([
+            'id'       => 1,
+            'username' => 'John',
+        ]);
+        $user->email         = 'john@example.org';
+        $user->password_hash = $hashedPassword;
+
+        $result = $passwords->check($plainPassword, $user);
+
+        $this->assertTrue($result->isOK());
+
+        return $hashedPassword;
+    }
+
+    /**
+     * @depends testHash
+     */
+    public function testNeedsRehashTakesCareOptions(string $hashedPassword): void
+    {
+        $config           = new AuthConfig();
+        $config->hashCost = 12;
+        $passwords        = new Passwords($config);
+
+        $result = $passwords->needsRehash($hashedPassword);
+
+        $this->assertTrue($result);
+    }
 }

programming_language: PHP