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:
tim
2026-09-26 15:27:51 +02:00
parent c8030b759d
commit 061f9c1a37
3 changed files with 42 additions and 4 deletions
+3 -4
View File
@@ -29,14 +29,13 @@ if (isset($_SESSION['editor'])) {
$getid = $db->execute_query($query, array($username)) or db_fail();
$row = $getid->fetch_assoc();
$stored_password = '';
$stored_password = NULL;
if (isset($row))
$stored_password = $row['password'];
$stored_password = (string)$row['password'];
else $denied = TRUE;
$getid->close();
$compare_password = is_string($_POST['password'] ?? NULL) ? $_POST['password'] : '';
// password_verify() also accepts hashes made by the old crypt() call
if (!password_verify($compare_password, $stored_password)) {
if (!editor_password_valid($compare_password, $stored_password)) {
$denied = TRUE;
record_login_failure($username);
open_db('close');
+15
View File
@@ -211,6 +211,21 @@ function password_hash_is_stored($username, $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
* current algorithm. If the column is too short and MySQL cut the new
+24
View File
@@ -23,6 +23,30 @@ class PasswordTest extends TestCase {
$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 {
$this->addEditor('old');
$hash = password_hash('secret', password_algorithm());