From c5ce7661f57ff6dc14fa8cc33adf60501f44a335 Mon Sep 17 00:00:00 2001
From: Timothy Allen editor login
Too many failed logins. Please try again in $wait_minutes minute".($wait_minutes == 1 ? "" : "s").".
Access denied. Please try again.
\n"; ?> diff --git a/session.lib.php b/session.lib.php index 639c4d6..72422d2 100644 --- a/session.lib.php +++ b/session.lib.php @@ -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 +}