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:
+9
-1
@@ -5,6 +5,7 @@ if ( ! isset($_SERVER['HTTPS'])) {
|
|||||||
}
|
}
|
||||||
require_once("../session.lib.php");
|
require_once("../session.lib.php");
|
||||||
$denied = NULL;
|
$denied = NULL;
|
||||||
|
$wait_minutes = 0;
|
||||||
$is_admin = NULL;
|
$is_admin = NULL;
|
||||||
if (isset($_SESSION['editor'])) {
|
if (isset($_SESSION['editor'])) {
|
||||||
session_write_close();
|
session_write_close();
|
||||||
@@ -12,6 +13,9 @@ if (isset($_SESSION['editor'])) {
|
|||||||
return;
|
return;
|
||||||
} else if (isset($_POST['login']) && !csrf_valid()) {
|
} else if (isset($_POST['login']) && !csrf_valid()) {
|
||||||
$denied = TRUE;
|
$denied = TRUE;
|
||||||
|
} else if (isset($_POST['login']) && ($wait = login_blocked_for((string)($_POST['username'] ?? ''))) > 0) {
|
||||||
|
$denied = TRUE;
|
||||||
|
$wait_minutes = (int)ceil($wait / 60);
|
||||||
} else if (isset($_POST['login'])) {
|
} else if (isset($_POST['login'])) {
|
||||||
include_once('../open_db.lib.php');
|
include_once('../open_db.lib.php');
|
||||||
$table = "editors";
|
$table = "editors";
|
||||||
@@ -31,12 +35,14 @@ if (isset($_SESSION['editor'])) {
|
|||||||
// password_verify() also accepts hashes made by the old crypt() call
|
// password_verify() also accepts hashes made by the old crypt() call
|
||||||
if (!password_verify($compare_password, $stored_password)) {
|
if (!password_verify($compare_password, $stored_password)) {
|
||||||
$denied = TRUE;
|
$denied = TRUE;
|
||||||
|
record_login_failure($username);
|
||||||
open_db('close');
|
open_db('close');
|
||||||
} else {
|
} else {
|
||||||
// Move an old hash to the current algorithm while we have the password
|
// Move an old hash to the current algorithm while we have the password
|
||||||
rehash_password($username, $compare_password, $stored_password);
|
rehash_password($username, $compare_password, $stored_password);
|
||||||
open_db('close');
|
open_db('close');
|
||||||
login_editor($username);
|
login_editor($username);
|
||||||
|
clear_login_failures($username);
|
||||||
// Keep user logged in with cookie, if they request it
|
// Keep user logged in with cookie, if they request it
|
||||||
if (isset($_POST['remember'])) {
|
if (isset($_POST['remember'])) {
|
||||||
create_persistent_login($username);
|
create_persistent_login($username);
|
||||||
@@ -81,7 +87,9 @@ if (isset($_SESSION['editor'])) {
|
|||||||
<H1>editor login</H1>
|
<H1>editor login</H1>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
if ($denied)
|
if ($wait_minutes)
|
||||||
|
echo "<p><strong>Too many failed logins. Please try again in $wait_minutes minute".($wait_minutes == 1 ? "" : "s").".</strong></p>\n";
|
||||||
|
else if ($denied)
|
||||||
echo "<p><strong>Access denied. Please try again.</strong></p>\n";
|
echo "<p><strong>Access denied. Please try again.</strong></p>\n";
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user