Skip to content

Commit ea9688d

Browse files
kenjisMGatner
andauthored
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 <[email protected]> * 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 <[email protected]>
1 parent 247c53f commit ea9688d

File tree

26 files changed

+435
-17
lines changed

26 files changed

+435
-17
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,4 @@ within this library, in no particular order:
8686
- [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)
8787
- [NIST Digital Identity Guidelines](https://pages.nist.gov/800-63-3/sp800-63b.html)
8888
- [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)
89+
- [Password Storage - OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)

UPGRADING.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Upgrade Guide
2+
3+
## Version 1.0.0-beta.3 to 1.0.0-beta.4
4+
5+
### Important Password Changes
6+
7+
#### Password Incompatibility
8+
9+
Shield 1.0.0-beta.4 fixes a [vulnerability related to password storage](https://github.com/codeigniter4/shield/security/advisories/GHSA-c5vj-f36q-p9vg).
10+
As a result, hashed passwords already stored in the database are no longer compatible
11+
and cannot be used by default.
12+
13+
All hashed passwords stored in Shield v1.0.0-beta.3 or earlier are easier to
14+
crack than expected due to the above vulnerability. Therefore, they should be
15+
removed as soon as possible.
16+
17+
Existing users will no longer be able to log in with their passwords and will
18+
need to log in with the magic link and then set their passwords again.
19+
20+
#### If You Want to Allow Login with Existing Passwords
21+
22+
If you want to use passwords saved in Shield v1.0.0-beta.3 or earlier,
23+
you must add the following property in `app/Config/Auth.php`:
24+
25+
```php
26+
public bool $supportOldDangerousPassword = true;
27+
```
28+
29+
After upgrading, with the above setting, once a user logs in with the password,
30+
the hashed password is updated and stored in the database.
31+
32+
In this case, the existing hashed passwords are still easier to crack than expected.
33+
Therefore, this setting should not be used for an extended period of time.
34+
So you should change the setting to `false` as soon as possible, and remove old
35+
hashed password.
36+
37+
> **Note**
38+
>
39+
> This setting is deprecated. It will be removed in v1.0.0 official release.
40+
41+
#### Limitations for the Default Password Handling
42+
43+
By default, Shield uses the hashing algorithm `PASSWORD_DEFAULT` (see `app/Config/Auth.php`),
44+
that is, `PASSWORD_BCRYPT` at the time of writing.
45+
46+
Now there are two limitations when you use `PASSWORD_BCRYPT`.
47+
48+
1. the password will be truncated to a maximum length of 72 bytes.
49+
2. the password will be truncated at the first NULL byte (`\0`).
50+
51+
If these behaviors are unacceptable, see [How to Strengthen the Password](https://github.com/codeigniter4/shield/blob/develop/docs/guides/strengthen_password.md).

docs/guides/strengthen_password.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# How to Strengthen the Password
2+
3+
Shield allows you to customize password-related settings to make your passwords more secure.
4+
5+
## Minimum Password Length
6+
7+
The most important factor when it comes to passwords is the number of characters in the password.
8+
You can check password strength with [Password Strength Testing Tool](https://bitwarden.com/password-strength/).
9+
Short passwords may be cracked in less than one day.
10+
11+
In Shield, you can set the users' minimum password length. The setting is
12+
`$minimumPasswordLength` in `app/Config/Auth.php`. The default value is 8 characters.
13+
It is the recommended minimum value by NIST. However, some organizations recommend
14+
12 to 14 characters.
15+
16+
The longer the password, the stronger it is. Consider increasing the value.
17+
18+
> **Note**
19+
>
20+
> This checking works when you validate passwords with the `strong_password`
21+
> validation rule.
22+
>
23+
> If you disable `CompositionValidator` (enabled by default) in `$passwordValidators`,
24+
> this checking will not work.
25+
26+
## Password Hashing Algorithm
27+
28+
You can change the password hashing algorithm by `$hashAlgorithm` in `app/Config/Auth.php`.
29+
The default value is `PASSWORD_DEFAULT` that is `PASSWORD_BCRYPT` at the time of writing.
30+
31+
`PASSWORD_BCRYPT` means to create new password hashes using the bcrypt algorithm.
32+
33+
You can use `PASSWORD_ARGON2ID` if your PHP has been compiled with Argon2 support.
34+
35+
### PASSWORD_BCRYPT
36+
37+
`PASSWORD_BCRYPT` has one configuration `$hashCost`. The bigger the cost, hashed passwords will be the stronger.
38+
39+
You can find your appropriate cost with the following code:
40+
41+
```php
42+
<?php
43+
/**
44+
* This code will benchmark your server to determine how high of a cost you can
45+
* afford. You want to set the highest cost that you can without slowing down
46+
* you server too much. 8-10 is a good baseline, and more is good if your servers
47+
* are fast enough. The code below aims for ≤ 50 milliseconds stretching time,
48+
* which is a good baseline for systems handling interactive logins.
49+
*
50+
* From: https://www.php.net/manual/en/function.password-hash.php#refsect1-function.password-hash-examples
51+
*/
52+
$timeTarget = 0.05; // 50 milliseconds
53+
54+
$cost = 8;
55+
do {
56+
$cost++;
57+
$start = microtime(true);
58+
password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
59+
$end = microtime(true);
60+
} while (($end - $start) < $timeTarget);
61+
62+
echo "Appropriate Cost Found: " . $cost;
63+
```
64+
65+
#### Limitations
66+
67+
There are two limitations when you use `PASSWORD_BCRYPT`:
68+
69+
1. the password will be truncated to a maximum length of 72 bytes.
70+
2. the password will be truncated at the first NULL byte (`\0`).
71+
72+
##### 72 byte issue
73+
74+
If a user submits a password longer than 72 bytes, the validation error will occur.
75+
If this behavior is unacceptable, consider:
76+
77+
1. change the hashing algorithm to `PASSWORD_ARGON2ID`. It does not have such a limitation.
78+
79+
##### NULL byte issue
80+
81+
This is because `PASSWORD_BCRYPT` is not binary-safe. Normal users cannot
82+
send NULL bytes in a password string, so this is not a problem in most cases.
83+
84+
But if this behavior is unacceptable, consider:
85+
86+
1. adding a validation rule to prohibit NULL bytes or control codes.
87+
2. or change the hashing algorithm to `PASSWORD_ARGON2ID`. It is binary-safe.
88+
89+
### PASSWORD_ARGON2ID
90+
91+
`PASSWORD_ARGON2ID` has three configuration `$hashMemoryCost`, `$hashTimeCost`,
92+
and `$hashThreads`.
93+
94+
If you use `PASSWORD_ARGON2ID`, you should use PHP's constants:
95+
96+
```php
97+
public int $hashMemoryCost = PASSWORD_ARGON2_DEFAULT_MEMORY_COST;
98+
99+
public int $hashTimeCost = PASSWORD_ARGON2_DEFAULT_TIME_COST;
100+
public int $hashThreads = PASSWORD_ARGON2_DEFAULT_THREADS;
101+
```
102+
103+
## Maximum Password Length
104+
105+
By default, Shield has the validation rules for maximum password length.
106+
107+
- 72 bytes for PASSWORD_BCRYPT
108+
- 255 characters for others
109+
110+
You can customize the validation rule. See [Customizing Shield](../customization.md).
111+
112+
## $supportOldDangerousPassword
113+
114+
In `app/Config/Auth.php` there is `$supportOldDangerousPassword`, which is a
115+
setting for using passwords stored in older versions of Shield that were [vulnerable](https://github.com/codeigniter4/shield/security/advisories/GHSA-c5vj-f36q-p9vg).
116+
117+
This setting is deprecated. If you have this setting set to `true`, you should change
118+
it to `false` as soon as possible, and remove old hashed password in your database.
119+
120+
> **Note**
121+
>
122+
> This setting will be removed in v1.0.0 official release.

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@
1414
## Guides
1515
* [Protecting an API with Access Tokens](guides/api_tokens.md)
1616
* [Mobile Authentication with Access Tokens](guides/mobile_apps.md)
17+
* [How to Strengthen the Password](guides/strengthen_password.md)

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ nav:
5151
- Guides:
5252
- guides/api_tokens.md
5353
- guides/mobile_apps.md
54+
- guides/strengthen_password.md

src/Authentication/Authenticators/Session.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -336,19 +336,30 @@ public function check(array $credentials): Result
336336
/** @var Passwords $passwords */
337337
$passwords = service('passwords');
338338

339+
// This is only for supportOldDangerousPassword.
340+
$needsRehash = false;
341+
339342
// Now, try matching the passwords.
340343
if (! $passwords->verify($givenPassword, $user->password_hash)) {
341-
return new Result([
342-
'success' => false,
343-
'reason' => lang('Auth.invalidPassword'),
344-
]);
344+
if (
345+
! setting('Auth.supportOldDangerousPassword')
346+
|| ! $passwords->verifyDanger($givenPassword, $user->password_hash) // @phpstan-ignore-line
347+
) {
348+
return new Result([
349+
'success' => false,
350+
'reason' => lang('Auth.invalidPassword'),
351+
]);
352+
}
353+
354+
// Passed with old dangerous password.
355+
$needsRehash = true;
345356
}
346357

347358
// Check to see if the password needs to be rehashed.
348359
// This would be due to the hash algorithm or hash
349360
// cost changing since the last time that a user
350361
// logged in.
351-
if ($passwords->needsRehash($user->password_hash)) {
362+
if ($passwords->needsRehash($user->password_hash) || $needsRehash) {
352363
$user->password_hash = $passwords->hash($givenPassword);
353364
$this->provider->save($user);
354365
}

src/Authentication/Passwords.php

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,42 @@ public function __construct(Auth $config)
3131
*/
3232
public function hash(string $password)
3333
{
34-
if ((defined('PASSWORD_ARGON2I') && $this->config->hashAlgorithm === PASSWORD_ARGON2I)
34+
return password_hash($password, $this->config->hashAlgorithm, $this->getHashOptions());
35+
}
36+
37+
private function getHashOptions(): array
38+
{
39+
if (
40+
(defined('PASSWORD_ARGON2I') && $this->config->hashAlgorithm === PASSWORD_ARGON2I)
3541
|| (defined('PASSWORD_ARGON2ID') && $this->config->hashAlgorithm === PASSWORD_ARGON2ID)
3642
) {
37-
$hashOptions = [
43+
return [
3844
'memory_cost' => $this->config->hashMemoryCost,
3945
'time_cost' => $this->config->hashTimeCost,
4046
'threads' => $this->config->hashThreads,
4147
];
42-
} else {
43-
$hashOptions = [
44-
'cost' => $this->config->hashCost,
45-
];
4648
}
4749

50+
return [
51+
'cost' => $this->config->hashCost,
52+
];
53+
}
54+
55+
/**
56+
* Hash a password.
57+
*
58+
* @return false|string|null
59+
*
60+
* @deprecated This is only for backward compatibility.
61+
*/
62+
public function hashDanger(string $password)
63+
{
4864
return password_hash(
4965
base64_encode(
5066
hash('sha384', $password, true)
5167
),
5268
$this->config->hashAlgorithm,
53-
$hashOptions
69+
$this->getHashOptions()
5470
);
5571
}
5672

@@ -61,6 +77,19 @@ public function hash(string $password)
6177
* @param string $hash The previously hashed password
6278
*/
6379
public function verify(string $password, string $hash): bool
80+
{
81+
return password_verify($password, $hash);
82+
}
83+
84+
/**
85+
* Verifies a password against a previously hashed password.
86+
*
87+
* @param string $password The password we're checking
88+
* @param string $hash The previously hashed password
89+
*
90+
* @deprecated This is only for backward compatibility.
91+
*/
92+
public function verifyDanger(string $password, string $hash): bool
6493
{
6594
return password_verify(base64_encode(
6695
hash('sha384', $password, true)
@@ -72,7 +101,7 @@ public function verify(string $password, string $hash): bool
72101
*/
73102
public function needsRehash(string $hashedPassword): bool
74103
{
75-
return password_needs_rehash($hashedPassword, $this->config->hashAlgorithm);
104+
return password_needs_rehash($hashedPassword, $this->config->hashAlgorithm, $this->getHashOptions());
76105
}
77106

78107
/**
@@ -110,4 +139,16 @@ public function check(string $password, ?User $user = null): Result
110139
'success' => true,
111140
]);
112141
}
142+
143+
/**
144+
* Returns the validation rule for max length.
145+
*/
146+
public static function getMaxLenghtRule(): string
147+
{
148+
if (config('Auth')->hashAlgorithm === PASSWORD_BCRYPT) {
149+
return 'max_byte[72]';
150+
}
151+
152+
return 'max_length[255]';
153+
}
113154
}

src/Authentication/Passwords/ValidationRules.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ public function strong_password(string $value, ?string &$error1 = null, array $d
5555
return $result->isOk();
5656
}
5757

58+
/**
59+
* Returns true if $str is $val or fewer bytes in length.
60+
*/
61+
public function max_byte(?string $str, string $val): bool
62+
{
63+
return is_numeric($val) && $val >= strlen($str ?? '');
64+
}
65+
5866
/**
5967
* Builds a new user instance from the global request.
6068
*/

src/Config/Auth.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,16 @@ class Auth extends BaseConfig
360360
*/
361361
public int $hashCost = 10;
362362

363+
/**
364+
* If you need to support passwords saved in versions prior to Shield v1.0.0-beta.4.
365+
* set this to true.
366+
*
367+
* See https://github.com/codeigniter4/shield/security/advisories/GHSA-c5vj-f36q-p9vg
368+
*
369+
* @deprecated This is only for backward compatibility.
370+
*/
371+
public bool $supportOldDangerousPassword = false;
372+
363373
/**
364374
* ////////////////////////////////////////////////////////////////////
365375
* OTHER SETTINGS

src/Controllers/LoginController.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use App\Controllers\BaseController;
88
use CodeIgniter\HTTP\RedirectResponse;
99
use CodeIgniter\Shield\Authentication\Authenticators\Session;
10+
use CodeIgniter\Shield\Authentication\Passwords;
1011
use CodeIgniter\Shield\Traits\Viewable;
1112

1213
class LoginController extends BaseController
@@ -90,8 +91,11 @@ protected function getValidationRules(): array
9091
'rules' => config('AuthSession')->emailValidationRules,
9192
],
9293
'password' => [
93-
'label' => 'Auth.password',
94-
'rules' => 'required',
94+
'label' => 'Auth.password',
95+
'rules' => 'required|' . Passwords::getMaxLenghtRule(),
96+
'errors' => [
97+
'max_byte' => 'Auth.errorPasswordTooLongBytes',
98+
],
9599
],
96100
];
97101
}

0 commit comments

Comments
 (0)