4
I’m reinforcing an authentication system, where in the password recovery part, I implemented it as follows.
- The user informs the e-mail address.
- If it exists, the e-mail is sent (see example below).
- The user clicks on the email and goes to the recovery page (through URL with code)
- The code is filled automatically in an Hidden field of the form (sanitization and exhaust ok).
- If the user enters the two correct passwords and the code is still valid, the password is changed.
The automatically generated email points to the link
"...../admin/session/reset/FYLPB557EKLGMPV6IYNOT2TF5MZUZM2ZB6O43PEUH5XOHCTBX42TLMLJZHSHZKF77V4ZEXELICSAOHDNTJJ4R2Z67R4JZ4ST72JOVWKK2BQCKRFQMQWJNFF2LQIHJ55C7FH5GO3LIMH4WEF3J6HOU4AQQVG3LVBKV7O3DDARB6IQVCGGUFI6RUC2QUYEIOHH"
admin
is my namespace, session
is the Controller, reset
the method and code is the parameter.
To verify that the code is valid, the database returns the ID of the user who requested the reset, where the "secret" is identical to the code, that is, if it exists and if it has not expired (10-minute limit). Note that this ID is never sent to the customer. This way the bot or person who is trying to hack, will not know the user ID and there will be no risk of a possible "date tamper" for example.
The whole process uses the HTTPS procotol.
The string is randomly generated, can only contain characters of A-Z0-9 and has 192 characters, through the following function (being concatenated, once 128 and the second 32).
public function createSecretKey($secretLength = 128)
{
$validChars = $this->_getBase32LookupTable();
// Valid secret lengths are 80 to 640 bits
if ($secretLength < 16 || $secretLength > 128)
{
throw new Exception('Bad secret length');
}
$secret = '';
$rnd = false;
if (function_exists('random_bytes'))
{
$rnd = random_bytes($secretLength);
}
elseif (function_exists('mcrypt_create_iv'))
{
$rnd = mcrypt_create_iv($secretLength, MCRYPT_DEV_URANDOM);
}
elseif (function_exists('openssl_random_pseudo_bytes'))
{
$rnd = openssl_random_pseudo_bytes($secretLength, $cryptoStrong);
if (!$cryptoStrong)
{
$rnd = false;
}
}
if ($rnd !== false)
{
for ($i = 0; $i < $secretLength; ++$i) {
$secret .= $validChars[ord($rnd[$i]) & 31];
}
}
else
{
throw new Exception('No source of secure random');
}
return $secret;
}
protected function _getBase32LookupTable()
{
return array(
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
'=', // padding char
);
}
The form to send the password reset request has 1 send limit every 10 minutes.
Considering the logic described, and that the system blocks the session and the IP every 10 incorrect attempts, and invalidates the code immediately when the user recovers the password or after 10 minutes. What would be the possibility of this URL being broken by some bot? Is there any weak point in this logic that can be broken? (not speaking at the level of code or vulnerabilities in programming, but in terms of logic).
Bots usually fill in all fields, including the ones that are Hidden, you can place an Hidden field and if it contains any value you cancel the token. You have an answer on that approach - very useful by the way.
– Papa Charlie
What if two separate user password reset requests are made during this period? Taking only the last record of the table, one of them will be invalidated.
– Woss
Sorry, I expressed myself a little poorly on this part. I edited the question. No problem two simultaneous requests from different users. 1 user can only request a new code every 10 minutes. The question of returning the last line was an additional check I had done but there was no need to describe it here as this was before adding the criterion of limiting 1 code every 10 minutes.
– Renan Cavalieri