Protection of WordPress logins
This commit is contained in:
282
modules/LimitLoginAttempts/Controllers/LoginHandler.php
Normal file
282
modules/LimitLoginAttempts/Controllers/LoginHandler.php
Normal file
@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
namespace Bdp\Modules\LimitLoginAttempts\Controllers;
|
||||
|
||||
use ProtectLogin\Modules\LimitLoginAttempts\Requests\IpAddress;
|
||||
|
||||
class LoginHandler {
|
||||
public const DIRECT_ADDR = 'REMOTE_ADDR';
|
||||
public const PROXY_ADDR = 'HTTP_X_FORWARDED_FOR';
|
||||
|
||||
public function onSuccessFullLogin($user, $password) {
|
||||
if (!is_wp_error($user) && $this->isLoginAllowedFromIp() ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
global $limit_login_my_error_shown;
|
||||
$limit_login_my_error_shown = true;
|
||||
|
||||
$error = new \WP_Error();
|
||||
// This error should be the same as in "shake it" filter below
|
||||
$error->add('too_many_retries', $this->composeErrorMessage());
|
||||
return $error;
|
||||
}
|
||||
|
||||
public function onFailedLogin(string $username) {
|
||||
$ip = $this->getAddress();
|
||||
|
||||
/* if currently locked-out, do not add to retries */
|
||||
$lockouts = get_option('protect_login_limit_login_lockouts', []);
|
||||
|
||||
if(isset($lockouts[$ip]) && time() < $lockouts[$ip]) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Get the arrays with retries and retries-valid information */
|
||||
$retries = get_option('kompass_limit_login_retries', []);
|
||||
$valid = get_option('kompass_limit_login_retries_valid', []);
|
||||
|
||||
/* Check validity and add one to retries */
|
||||
if (isset($retries[$ip])) { //} && isset($valid[$ip]) && time() < $valid[$ip]) {
|
||||
$retries[$ip] ++;
|
||||
} else {
|
||||
$retries[$ip] = 1;
|
||||
}
|
||||
|
||||
update_option('kompass_limit_login_retries', $retries);
|
||||
|
||||
/* lockout? */
|
||||
if($retries[$ip] % get_option('kompass_limit_login_allowed_retries', 0) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$retries_long = get_option('kompass_limit_login_allowed_retries', 1)
|
||||
* get_option('kompass_limit_login_allowed_lockouts', 1);
|
||||
|
||||
if ($retries[$ip] >= $retries_long) {
|
||||
$lockouts[$ip] = time() + get_option('kompass_limit_login_long_duration', 86400);
|
||||
|
||||
} else {
|
||||
$lockouts[$ip] = time() + get_option('kompass_limit_login_lockout_duration', 900);
|
||||
}
|
||||
|
||||
update_option('kompass_limit_login_lockouts', $lockouts);
|
||||
|
||||
|
||||
/* do any notification */
|
||||
$this->notify($username);
|
||||
|
||||
}
|
||||
|
||||
private function notifyByEmail($user)
|
||||
{
|
||||
$ip = $this->getAddress();
|
||||
|
||||
$lockouts = get_option('kompass_limit_login_lockouts', []);
|
||||
if (!isset($lockouts[$ip])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$blocked_until = $lockouts[$ip];
|
||||
|
||||
$retries = get_option('kompass_limit_login_retries', []);
|
||||
$currentRetries = $retries[$ip];
|
||||
|
||||
$notify_after = get_option('kompass_limit_login_notify_email_after', 1);
|
||||
if ($currentRetries % $notify_after !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$blogname = get_option('blogname', 'none');
|
||||
|
||||
$subject = sprintf(__("[%s] Too many failed login attempts"
|
||||
, 'limit-login-attempts')
|
||||
, $blogname);
|
||||
|
||||
$message = 'Neue Sperrung auf deiner Webseite: ' . PHP_EOL .
|
||||
'IP-Adresse: ' . $ip . PHP_EOL .
|
||||
'Gesperrt bis: ' . date('d.m.Y H:i', $blocked_until);
|
||||
|
||||
$admin_email = get_option('admin_email');
|
||||
wp_mail($admin_email, $subject, $message);
|
||||
}
|
||||
|
||||
|
||||
/* Handle notification in event of lockout */
|
||||
private function notify($user) {
|
||||
$args = get_option('kompass_limit_login_lockout_notify', []);
|
||||
if (!is_array($args)) {
|
||||
$args = [$args];
|
||||
}
|
||||
foreach ($args as $mode) {
|
||||
switch (trim($mode)) {
|
||||
case 'email':
|
||||
$this->notifyByEmail($user);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function composeErrorMessage() {
|
||||
$ip = $this->getAddress();
|
||||
$lockouts = get_option('kompass_limit_login_lockouts');
|
||||
|
||||
$msg = __('<strong>ERROR</strong>: Too many failed login attempts.', 'limit-login-attempts') . ' ';
|
||||
|
||||
if (!is_array($lockouts) || !isset($lockouts[$ip]) || time() >= $lockouts[$ip]) {
|
||||
/* Huh? No timeout active? */
|
||||
$msg .= __('Please try again later.', 'limit-login-attempts');
|
||||
return $msg;
|
||||
}
|
||||
|
||||
$when = ceil(($lockouts[$ip] - time()) / 60);
|
||||
if ($when > 60) {
|
||||
$when = ceil($when / 60);
|
||||
$msg .= sprintf(_n('Please try again in %d hour.', 'Please try again in %d hours.', $when, 'limit-login-attempts'), $when);
|
||||
} else {
|
||||
$msg .= sprintf(_n('Please try again in %d minute.', 'Please try again in %d minutes.', $when, 'limit-login-attempts'), $when);
|
||||
}
|
||||
|
||||
return $msg;
|
||||
}
|
||||
|
||||
private static function getAddress($typeName = '') {
|
||||
global $limitLoginAttemptsSettings;
|
||||
|
||||
$typeOriginal = $typeName;
|
||||
if (empty($typeName)) {
|
||||
$typeName = get_option('kompass_limit_loginclient_type', self::DIRECT_ADDR);
|
||||
}
|
||||
|
||||
if (isset($_SERVER[$typeName]) && filter_var($_SERVER[$typeName], FILTER_VALIDATE_IP)) {
|
||||
return $_SERVER[$typeName];
|
||||
}
|
||||
|
||||
/*
|
||||
* Not found. Did we get proxy type from option?
|
||||
* If so, try to fall back to direct address.
|
||||
*/
|
||||
if ( empty($typeName) && $typeOriginal == self::PROXY_ADDR
|
||||
&& isset($_SERVER[self::DIRECT_ADDR])
|
||||
&& filter_var($_SERVER[self::DIRECT_ADDR], FILTER_VALIDATE_IP)) {
|
||||
|
||||
/*
|
||||
* NOTE: Even though we fall back to direct address -- meaning you
|
||||
* can get a mostly working plugin when set to PROXY mode while in
|
||||
* fact directly connected to Internet it is not safe!
|
||||
*
|
||||
* Client can itself send HTTP_X_FORWARDED_FOR header fooling us
|
||||
* regarding which IP should be banned.
|
||||
*/
|
||||
|
||||
return $_SERVER[self::DIRECT_ADDR];
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
}
|
||||
|
||||
public function isLoginAllowedFromIp() {
|
||||
$ip = $this->getAddress();
|
||||
|
||||
if (in_array($ip, get_option('kompass_limit_login_blocklist', []))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (in_array($ip, get_option('kompass_limit_login_allowlist', []))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* lockout active? */
|
||||
$lockouts = get_option('kompass_limit_login_lockouts', []);
|
||||
return (!is_array($lockouts) || !isset($lockouts[$ip]) || time() >= $lockouts[$ip]);
|
||||
}
|
||||
|
||||
public function checkFailedCookies($cookie_elements) {
|
||||
$this->clearAuthCookie();
|
||||
|
||||
/*
|
||||
* Invalid username gets counted every time.
|
||||
*/
|
||||
|
||||
$this->onFailedLogin($cookie_elements['username']);
|
||||
}
|
||||
|
||||
private function clearAuthCookie() {
|
||||
wp_clear_auth_cookie();
|
||||
|
||||
if (!empty($_COOKIE[AUTH_COOKIE])) {
|
||||
$_COOKIE[AUTH_COOKIE] = '';
|
||||
}
|
||||
if (!empty($_COOKIE[SECURE_AUTH_COOKIE])) {
|
||||
$_COOKIE[SECURE_AUTH_COOKIE] = '';
|
||||
}
|
||||
if (!empty($_COOKIE[LOGGED_IN_COOKIE])) {
|
||||
$_COOKIE[LOGGED_IN_COOKIE] = '';
|
||||
}
|
||||
}
|
||||
|
||||
public function onValidCookie($cookie_elements, $user) {
|
||||
/*
|
||||
* As all meta values get cached on user load this should not require
|
||||
* any extra work for the common case of no stored value.
|
||||
*/
|
||||
|
||||
if (get_user_meta($user->ID, 'kompass_limit_login_previous_cookie')) {
|
||||
delete_user_meta($user->ID, 'kompass_limit_login_previous_cookie');
|
||||
}
|
||||
}
|
||||
|
||||
function clearLoginCookie($cookie_elements) {
|
||||
$this->clearAuthCookie();
|
||||
|
||||
/*
|
||||
* Under some conditions an invalid auth cookie will be used multiple
|
||||
* times, which results in multiple failed attempts from that one
|
||||
* cookie.
|
||||
*
|
||||
* Unfortunately I've not been able to replicate this consistently and
|
||||
* thus have not been able to make sure what the exact cause is.
|
||||
*
|
||||
* Probably it is because a reload of for example the admin dashboard
|
||||
* might result in multiple requests from the browser before the invalid
|
||||
* cookie can be cleard.
|
||||
*
|
||||
* Handle this by only counting the first attempt when the exact same
|
||||
* cookie is attempted for a user.
|
||||
*/
|
||||
|
||||
extract($cookie_elements, EXTR_OVERWRITE);
|
||||
|
||||
// Check if cookie is for a valid user
|
||||
$user = get_user_by('login', $username);
|
||||
if (!$user) {
|
||||
// "shouldn't happen" for this action
|
||||
$this->onFailedLogin($username);
|
||||
return;
|
||||
}
|
||||
|
||||
$previous_cookie = get_user_meta($user->ID, 'kompass_limit_login_previous_cookie', true);
|
||||
if ($previous_cookie && $previous_cookie == $cookie_elements) {
|
||||
// Identical cookies, ignore this attempt
|
||||
return;
|
||||
}
|
||||
|
||||
// Store cookie
|
||||
if ($previous_cookie)
|
||||
update_user_meta($user->ID, 'kompass_limit_login_previous_cookie', $cookie_elements);
|
||||
else
|
||||
add_user_meta($user->ID, 'kompass_limit_login_previous_cookie', $cookie_elements, true);
|
||||
|
||||
$this->onFailedLogin($username);
|
||||
}
|
||||
|
||||
public function handleCookies() {
|
||||
if ($this->isLoginAllowedFromIp()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->clearAuthCookie();
|
||||
}
|
||||
}
|
130
modules/LimitLoginAttempts/Controllers/OptionsPage.php
Normal file
130
modules/LimitLoginAttempts/Controllers/OptionsPage.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Bdp\Modules\LimitLoginAttempts\Controllers;
|
||||
|
||||
|
||||
class OptionsPage
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
add_options_page(BDP_LV_PLUGIN_SLUG . '-limit-login-attempts',
|
||||
'Protect Login',
|
||||
'site-health.php',
|
||||
BDP_LV_PLUGIN_SLUG . '-limit-login-attempts',
|
||||
[$this, 'limit_login_option_page'],2048);
|
||||
}
|
||||
|
||||
public function releaseIp($ip)
|
||||
{
|
||||
$allIps = get_option('kompass_limit_login_lockouts', []);
|
||||
unset($allIps[$ip]);
|
||||
update_option('kompass_limit_login_lockouts', $allIps);
|
||||
}
|
||||
|
||||
public function getBlockedIps()
|
||||
{
|
||||
$ips = '';
|
||||
foreach (get_option('kompass_limit_login_lockouts', []) as $ip => $blockedUntil) {
|
||||
$ips .= '<tr>' .
|
||||
'<td style="padding-right: 10px;">' . $ip . '</td>' .
|
||||
'<td style="padding-right: 10px;">' . date('d.m.Y H:i', $blockedUntil) . ' Uhr</td>' .
|
||||
'<td>
|
||||
<a href="admin.php?page=bdp-kompass-limit-login-attempts&tab=tab4&action=release&ip=' .
|
||||
base64_encode($ip) . '">Freigeben</a></td>' .
|
||||
'</tr>';
|
||||
};
|
||||
|
||||
return $ips;
|
||||
}
|
||||
|
||||
public function limit_login_option_page() {
|
||||
global $errors;
|
||||
|
||||
$showMessage = null;
|
||||
|
||||
if (isset($_POST['update_options'])) {
|
||||
update_settings($_POST);
|
||||
$showMessage = 'Die Einstellungen wurden gespeichert';
|
||||
}
|
||||
if (isset($_GET['action']) && $_GET['action'] == 'release') {
|
||||
$showMessage = 'Die IP-Adresse wurde freigegeben.';
|
||||
}
|
||||
|
||||
if(isset($_POST['save_kompass_balist_list_type'])) {
|
||||
$showMessage = 'Die Liste wurde gespeichert.';
|
||||
}
|
||||
|
||||
if (null !== $showMessage && $errors === false) {
|
||||
echo '<div class="notice notice-success" style="padding: 5px 10px;">';
|
||||
echo $showMessage;
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
if ($errors) {
|
||||
echo '<div class="notice notice-error" style="padding: 5px 10px;">';
|
||||
echo 'Beim Durchführen der Aktion ist ein Fehler aufgetreten.';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
$tab = isset($_GET['tab']) ? $_GET['tab'] : 'tab1';
|
||||
?>
|
||||
|
||||
<div class="wrap">
|
||||
<h1 class="wp-heading-inline">Protect Login - Einstellungen</h1>
|
||||
<hr class="wp-header-end">
|
||||
<?= kompass_print_tab_header($tab); ?>
|
||||
|
||||
<div class="tab-content">
|
||||
<?php
|
||||
switch ($tab) {
|
||||
case 'tab1':
|
||||
echo '<form action="admin.php?page=bdp-kompass-limit-login-attempts&tab=tab1" method="post">';
|
||||
do_settings_sections(BDP_LV_PLUGIN_SLUG . '-limit-login-attempts');
|
||||
submit_button();
|
||||
echo '</form>';
|
||||
break;
|
||||
case 'tab2':
|
||||
echo '<h2>Blocklist</h2>';
|
||||
echo '<form action="admin.php?page=bdp-kompass-limit-login-attempts&tab=tab2" method="post">';
|
||||
kompass_print_block_allow_form('blocklist');
|
||||
submit_button();
|
||||
echo '</form>';
|
||||
break;
|
||||
case 'tab3':
|
||||
echo '<h2>Allowlist</h2>';
|
||||
echo '<form action="admin.php?page=bdp-kompass-limit-login-attempts&tab=tab3" method="post">';
|
||||
kompass_print_block_allow_form('allowlist');
|
||||
submit_button();
|
||||
echo '</form>';
|
||||
break;
|
||||
case 'tab4':
|
||||
if (isset($_GET['action']) && $_GET['action'] == 'release') {
|
||||
$this->releaseIp(base64_decode($_GET['ip']));
|
||||
}
|
||||
$blockedIps = $this->getBlockedIps();
|
||||
?>
|
||||
<h3>Gesperrte IPs</h3>
|
||||
<?php
|
||||
if (strlen($blockedIps) == 0) {
|
||||
echo '<div class="protect-login-no-blocked-ips">';
|
||||
echo 'Derzeit sind keine Adressen gesperrt.';
|
||||
echo '</div>';
|
||||
} else { ?>
|
||||
<table>
|
||||
<tr>
|
||||
<th>IP</th>
|
||||
<th>Gesperrt bis</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
<?= $blockedIps ?>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
break;
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user