Hash the password for an unknown username at login.
An unknown username got a fast answer, which showed which usernames exist.
This commit is contained in:
+3
-4
@@ -29,14 +29,13 @@ if (isset($_SESSION['editor'])) {
|
|||||||
$getid = $db->execute_query($query, array($username)) or db_fail();
|
$getid = $db->execute_query($query, array($username)) or db_fail();
|
||||||
$row = $getid->fetch_assoc();
|
$row = $getid->fetch_assoc();
|
||||||
|
|
||||||
$stored_password = '';
|
$stored_password = NULL;
|
||||||
if (isset($row))
|
if (isset($row))
|
||||||
$stored_password = $row['password'];
|
$stored_password = (string)$row['password'];
|
||||||
else $denied = TRUE;
|
else $denied = TRUE;
|
||||||
$getid->close();
|
$getid->close();
|
||||||
$compare_password = is_string($_POST['password'] ?? NULL) ? $_POST['password'] : '';
|
$compare_password = is_string($_POST['password'] ?? NULL) ? $_POST['password'] : '';
|
||||||
// password_verify() also accepts hashes made by the old crypt() call
|
if (!editor_password_valid($compare_password, $stored_password)) {
|
||||||
if (!password_verify($compare_password, $stored_password)) {
|
|
||||||
$denied = TRUE;
|
$denied = TRUE;
|
||||||
record_login_failure($username);
|
record_login_failure($username);
|
||||||
open_db('close');
|
open_db('close');
|
||||||
|
|||||||
@@ -211,6 +211,21 @@ function password_hash_is_stored($username, $hash) {
|
|||||||
return $row && $row['password'] === $hash;
|
return $row && $row['password'] === $hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TRUE if $password matches $stored_hash. For an unknown editor
|
||||||
|
* ($stored_hash is NULL), it hashes the password anyway, so that the
|
||||||
|
* answer takes as long as for a wrong password, and does not show which
|
||||||
|
* usernames exist.
|
||||||
|
*/
|
||||||
|
function editor_password_valid($password, $stored_hash) {
|
||||||
|
if ($stored_hash === NULL) {
|
||||||
|
password_hash($password, password_algorithm());
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
// password_verify() also accepts hashes made by the old crypt() call
|
||||||
|
return password_verify($password, $stored_hash);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* After a successful login, move an old hash (MD5-crypt, bcrypt) to the
|
* After a successful login, move an old hash (MD5-crypt, bcrypt) to the
|
||||||
* current algorithm. If the column is too short and MySQL cut the new
|
* current algorithm. If the column is too short and MySQL cut the new
|
||||||
|
|||||||
@@ -23,6 +23,30 @@ class PasswordTest extends TestCase {
|
|||||||
$this->assertSame(defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_BCRYPT, password_algorithm());
|
$this->assertSame(defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_BCRYPT, password_algorithm());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testEditorPasswordValid(): void {
|
||||||
|
$hash = password_hash('secret', password_algorithm());
|
||||||
|
$this->assertTrue(editor_password_valid('secret', $hash));
|
||||||
|
$this->assertFalse(editor_password_valid('wrong', $hash));
|
||||||
|
$this->assertFalse(editor_password_valid('', ''));
|
||||||
|
$this->assertFalse(editor_password_valid('secret', NULL));
|
||||||
|
$this->assertFalse(editor_password_valid('', NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A fast answer for an unknown username would show which usernames exist
|
||||||
|
public function testAnUnknownEditorTakesAsLongAsAWrongPassword(): void {
|
||||||
|
$hash = password_hash('secret', password_algorithm());
|
||||||
|
$time = function ($stored) {
|
||||||
|
$best = INF;
|
||||||
|
for ($i = 0; $i < 3; $i++) {
|
||||||
|
$start = hrtime(TRUE);
|
||||||
|
editor_password_valid('wrong', $stored);
|
||||||
|
$best = min($best, hrtime(TRUE) - $start);
|
||||||
|
}
|
||||||
|
return $best;
|
||||||
|
};
|
||||||
|
$this->assertGreaterThan($time($hash) / 2, $time(NULL));
|
||||||
|
}
|
||||||
|
|
||||||
public function testHashIsStoredAndChecked(): void {
|
public function testHashIsStoredAndChecked(): void {
|
||||||
$this->addEditor('old');
|
$this->addEditor('old');
|
||||||
$hash = password_hash('secret', password_algorithm());
|
$hash = password_hash('secret', password_algorithm());
|
||||||
|
|||||||
Reference in New Issue
Block a user