Limit failed logins.

At most 5 per username and address, 20 per address and 50 per username
in 15 minutes.
This commit is contained in:
tim
2026-09-25 21:46:38 +02:00
parent 0f00cf8f49
commit c5ce7661f5
2 changed files with 66 additions and 1 deletions
+57
View File
@@ -185,3 +185,60 @@ function rehash_password($username, $password, $stored_hash) {
error_log("xxx: could not store a new password hash for $username; widen editors.password");
}
}
/*
* Login throttling. Failed logins are counted in files outside the web
* root, per username and address, per address, and per username. Once a
* count reaches its limit within LOGIN_WINDOW, further attempts are
* refused without checking the password.
*/
define('LOGIN_WINDOW', 15 * 60); // seconds
function login_limits($username) {
$username = strtolower($username);
$address = $_SERVER['REMOTE_ADDR'] ?? '';
return array(
"user-ip:$username|$address" => 5,
"ip:$address" => 20,
"user:$username" => 50,
);
}
function login_throttle_file($key) {
$dir = sys_get_temp_dir() . '/xxx-login-throttle';
if (!is_dir($dir))
@mkdir($dir, 0700, TRUE);
return $dir . '/' . hash('sha256', $key);
}
// Times of the failed logins for $key within the window
function login_failures($key) {
$file = login_throttle_file($key);
$times = is_file($file) ? json_decode((string)@file_get_contents($file), TRUE) : array();
$since = time() - LOGIN_WINDOW;
return array_values(array_filter(is_array($times) ? $times : array(),
function ($time) use ($since) { return is_int($time) && $time > $since; }));
}
// Seconds until $username may try to log in again from this address, or 0
function login_blocked_for($username) {
$wait = 0;
foreach (login_limits($username) as $key => $limit) {
$times = login_failures($key);
if (count($times) >= $limit)
$wait = max($wait, min($times) + LOGIN_WINDOW - time());
}
return $wait;
}
function record_login_failure($username) {
foreach (array_keys(login_limits($username)) as $key) {
$times = login_failures($key);
$times[] = time();
file_put_contents(login_throttle_file($key), json_encode($times), LOCK_EX);
}
}
function clear_login_failures($username) {
$keys = array_keys(login_limits($username));
@unlink(login_throttle_file($keys[0])); // this username from this address
}