diff --git a/assets/password.js b/assets/password.js new file mode 100644 index 0000000..8796982 --- /dev/null +++ b/assets/password.js @@ -0,0 +1,43 @@ +jQuery(document).ready(function($) { + $( "" ).insertBefore( ".submit" ); + + $("#password_too_short").css('display', 'none'); + + $(document).on('DOMSubtreeModified', '#pass-strength-result', function() { + var strengthMeter = $(this).attr('class'); + var allowedStrengths = php_vars.allowed_strengths; + + $( "[name='pw_weak']" ).css('visibility', 'hidden'); + $( '.pw-weak' ).css('visibility', 'hidden'); + $( '#pw-weak-text-label' ).css('visibility', 'hidden'); + + if (strengthMeter !== '') { + if (allowedStrengths.includes(strengthMeter)) { + $("[name='pw_weak']").prop("checked", true); + $("[name='submit']").css('display', 'inline'); + $('#createusersub').css('display', 'inline'); + $('submit').onclick = function() { + $('your-profile').submit(); + }; + $("#createusersub").onclick = function() { + $('createuser').submit(); + }; + + $("#password_too_short").css('display', 'none'); + } else { + $("#createusersub").css('display', 'none'); + $("[name='submit']").prop("disabled", true); + $("[name='pw_weak']").prop("checked", false); + $("[name='submit']").css('display', 'none'); + $('submit').onclick = function() { + return false; + }; + $("#createusersub").onclick = function() { + return false; + }; + $("#password_too_short").css('display', 'inline'); + } + } + }); +}); \ No newline at end of file diff --git a/assets/security.css b/assets/security.css index 5e213a9..46884dd 100644 --- a/assets/security.css +++ b/assets/security.css @@ -50,4 +50,16 @@ .long_text { width: 80%; +} + +.protect-login-no-blocked-ips +{ + padding: 5px 10px; + width: 90%; + background-color: #ffffff; + border-style: solid; + border-color: #00a32a; + border-width: 1px; + font-weight: bold; + font-size: 12pt; } \ No newline at end of file diff --git a/bdp-kompass.php b/bdp-kompass.php index 1089ef6..fd253ce 100644 --- a/bdp-kompass.php +++ b/bdp-kompass.php @@ -12,6 +12,7 @@ * Text Domain: bdp-kompass */ +use Bdp\Modules\LimitLoginAttempts\Controllers\OptionsPage as OptionsPageAlias; use Bdp\Modules\Security\Security; use Bdp\Modules\Seo\Seo; @@ -36,6 +37,9 @@ function bdp_plugin_init() { } } +add_action('admin_menu', function () { + new OptionsPageAlias(); +}); function register_custom_theme_directory() { @@ -47,4 +51,12 @@ function register_custom_theme_directory() { switch_theme('buena'); } +function enqueue_custom_password_js() { + wp_enqueue_script( 'custom-password-js', BDP_LV_PLUGIN_URL . 'assets/password.js'); + wp_localize_script( 'custom-password-js', 'php_vars', [ + 'allowed_strengths' => kompass_get_minimal_password_strength(), + 'password_too_short_text' => 'Dass Passwort entspricht nicht den Anforderungen.' + ]); +} + #add_action( 'after_setup_theme', 'register_custom_theme_directory' ); diff --git a/includes/action_caller.php b/includes/action_caller.php new file mode 100644 index 0000000..9913ef1 --- /dev/null +++ b/includes/action_caller.php @@ -0,0 +1,23 @@ +handleCookies(); + add_action('auth_cookie_bad_username', [$loginHandler, 'checkFailedCookies']); + add_action('auth_cookie_valid', [$loginHandler, 'onValidCookie'], 10, 2); +} + +if (isset($_POST['save_kompass_balist_list_type'])) { + updateBlockOrAllowList($_POST); +} + + diff --git a/includes/frontend-functions.php b/includes/frontend-functions.php index a3b79df..e44b540 100644 --- a/includes/frontend-functions.php +++ b/includes/frontend-functions.php @@ -16,17 +16,13 @@ function bdp_update_dashboard_style() { function bdp_add_menu_security() { + $moduleLoad = get_admin_url() . 'admin.php?page=' . BDP_LV_PLUGIN_SLUG . '/modules/index.php&loadmodule='; - add_menu_page( - 'Sicherheit', - 'Webseiten-Sicherheit', - 'manage_options', - 'site-health.php', - '', - 'dashicons-admin-network', - 5 - ); + + + + } function bdp_add_menu_contents() { @@ -69,7 +65,7 @@ function bdp_add_menu_mein_lv() { $moduleLoad = get_admin_url() . 'admin.php?page=' . BDP_LV_PLUGIN_SLUG . '/modules/index.php&loadmodule='; add_menu_page( - 'Mein BDP', + 'Mein BdP', 'BdP', 'manage_options', $mainSlug, @@ -113,7 +109,7 @@ function bdp_add_menu_setup() { add_submenu_page('users.php', 'Design-Einstellungen', - 'Design', + 'Template bearbeiten', 'manage_options', 'customize.php?return=/wp-admin/' ); @@ -132,6 +128,22 @@ function bdp_add_menu_setup() { 'manage_options', 'themes.php' ); + + add_submenu_page('users.php', + 'Sicherheit', + 'Webseiten-Sicherheit', + 'manage_options', + 'site-health.php' + ); + + $loginOption = new \Bdp\Modules\LimitLoginAttempts\Controllers\OptionsPage(); + add_submenu_page('users.php', + 'Login-Sicherheit', + 'Login-Sicherheit', + 'manage_options', + BDP_LV_PLUGIN_SLUG . '-limit-login-attempts', + [$loginOption, 'limit_login_option_page'] + ); } function bdp_cleanup_menu() diff --git a/includes/pre_requires.php b/includes/pre_requires.php index 1436eff..12dc003 100644 --- a/includes/pre_requires.php +++ b/includes/pre_requires.php @@ -3,3 +3,4 @@ require_once (ABSPATH . '/wp-admin/includes/plugin.php'); require_once (ABSPATH . '/wp-admin/includes/class-wp-filesystem-base.php'); require_once (ABSPATH . '/wp-admin/includes/class-wp-filesystem-direct.php'); require_once (ABSPATH . '/wp-includes/pluggable.php'); +require_once (ABSPATH . '/wp-admin/includes/template.php'); \ No newline at end of file diff --git a/includes/setup.php b/includes/setup.php index f8b5e8d..ca1e6a2 100644 --- a/includes/setup.php +++ b/includes/setup.php @@ -3,9 +3,12 @@ if ( ! defined( 'WP_PLUGIN_DIR' ) ) { // Abspath to wp-content/plu define( 'WP_PLUGIN_DIR', WP_CONTENT_DIR . '/plugins' ); // Full path, no trailing slash. } +use Bdp\Modules\LimitLoginAttempts\Controllers\LoginHandler; + + require_once dirname(__FILE__) . '/pre_requires.php'; require_once dirname(__FILE__) . '/environment.php'; - +require_once dirname(__FILE__) . '/spl.php'; require_once dirname(__FILE__) . '/update.class.php'; require_once BDP_LV_PLUGIN_DIR . 'includes/FileAccess.class.php'; @@ -20,6 +23,10 @@ require_once (BDP_LV_PLUGIN_DIR . '/includes/frontend-functions.php'); require_once (BDP_LV_PLUGIN_DIR . '/modules/security/security.php'); +function admin_init() +{ + kompass_settings_validators(); +} bdp_create_menu_structure(); @@ -31,6 +38,7 @@ function bdp_kompass_load_plugin_textdomain() { -#$class = +$loginHandler = new LoginHandler(); new BdpVersionChecker(); #add_filter( 'plugins_api', array( $class, 'info' ), 20, 3 ); +require_once dirname(__FILE__) . '/action_caller.php'; diff --git a/includes/spl.php b/includes/spl.php new file mode 100644 index 0000000..5f3b398 --- /dev/null +++ b/includes/spl.php @@ -0,0 +1,32 @@ +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 = __('ERROR: 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(); + } +} \ No newline at end of file diff --git a/modules/LimitLoginAttempts/Controllers/OptionsPage.php b/modules/LimitLoginAttempts/Controllers/OptionsPage.php new file mode 100644 index 0000000..8e4f4f3 --- /dev/null +++ b/modules/LimitLoginAttempts/Controllers/OptionsPage.php @@ -0,0 +1,130 @@ + $blockedUntil) { + $ips .= '' . + '' . $ip . '' . + '' . date('d.m.Y H:i', $blockedUntil) . ' Uhr' . + ' + Freigeben' . + ''; + }; + + 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 '
'; + echo $showMessage; + echo '
'; + } + + if ($errors) { + echo '
'; + echo 'Beim Durchführen der Aktion ist ein Fehler aufgetreten.'; + echo '
'; + } + + $tab = isset($_GET['tab']) ? $_GET['tab'] : 'tab1'; + ?> + +
+

Protect Login - Einstellungen

+
+ + +
+ '; + do_settings_sections(BDP_LV_PLUGIN_SLUG . '-limit-login-attempts'); + submit_button(); + echo ''; + break; + case 'tab2': + echo '

Blocklist

'; + echo '
'; + kompass_print_block_allow_form('blocklist'); + submit_button(); + echo '
'; + break; + case 'tab3': + echo '

Allowlist

'; + echo '
'; + kompass_print_block_allow_form('allowlist'); + submit_button(); + echo '
'; + break; + case 'tab4': + if (isset($_GET['action']) && $_GET['action'] == 'release') { + $this->releaseIp(base64_decode($_GET['ip'])); + } + $blockedIps = $this->getBlockedIps(); + ?> +

Gesperrte IPs

+ '; + echo 'Derzeit sind keine Adressen gesperrt.'; + echo '
'; + } else { ?> + + + + + + + +
IPGesperrt bisAktion
+ +
+ + [ + 'email' => 'E-Mail an Administrator' + ], + ]; + + if(!isset($options[$settingName])) { + return; + } + + $setting = $options[$settingName]; + foreach ($setting as $radioOption => $optionText) { + $isChecked = in_array($radioOption, $currentSetting) ? 'checked ' : '' ; + + echo '' . + '
'; + } +} diff --git a/modules/LimitLoginAttempts/Views/radio-option.php b/modules/LimitLoginAttempts/Views/radio-option.php new file mode 100644 index 0000000..a317dfd --- /dev/null +++ b/modules/LimitLoginAttempts/Views/radio-option.php @@ -0,0 +1,35 @@ + [ + 'REMOTE_ADDR' => 'Direkte Verbrindung', + 'HTTP_X_FORWARDED_FOR' => 'Hinter einem Proxy' + ], + 'kompass_limit_login_cookies' => [ + true => 'Ja', + false => 'Nein' + ], + 'kompass_password_minimal_strength' => [ + '1' => 'Alle Passwörter erlauben', + '2' => 'Mittelstarke Passwörter', + '3' => 'Nur Starke Passwörter' + ] + ]; + + if(!isset($options[$settingName])) { + return; + } + + $setting = $options[$settingName]; + foreach ($setting as $radioOption => $optionText) { + $isChecked = $currentSetting == $radioOption ? 'checked ' : '' ; + echo '' . + '   '; + } +} diff --git a/modules/LimitLoginAttempts/Views/tab-control.php b/modules/LimitLoginAttempts/Views/tab-control.php new file mode 100644 index 0000000..c94a830 --- /dev/null +++ b/modules/LimitLoginAttempts/Views/tab-control.php @@ -0,0 +1,18 @@ +'. + ' + Optionen + '. + ' + Blocklist + '. + ' + Allowlist + '. + ' + Gesperrte IPs + '; + } \ No newline at end of file diff --git a/modules/LimitLoginAttempts/Views/text-element.php b/modules/LimitLoginAttempts/Views/text-element.php new file mode 100644 index 0000000..2702e0c --- /dev/null +++ b/modules/LimitLoginAttempts/Views/text-element.php @@ -0,0 +1,7 @@ +'; + if (defined('WP_DEBUG') && WP_DEBUG == true) { + echo '
' . $settingName; + } +} diff --git a/modules/LimitLoginAttempts/includes/block-and-allow-list-form.php b/modules/LimitLoginAttempts/includes/block-and-allow-list-form.php new file mode 100644 index 0000000..265584d --- /dev/null +++ b/modules/LimitLoginAttempts/includes/block-and-allow-list-form.php @@ -0,0 +1,56 @@ + + + + + +

+
+ +

+ + +
+

+

+ +

+
+'; +} + +function _kompass_limit_logins_settings_callback($args) { + $setting = get_option($args['setting'], null); + if (null === $setting) { + $setting = ''; + } + + $value = esc_attr($setting); + if (isset($args['unit_division'])) { + $value = (int)$value / (int)$args['unit_division']; + } + + kompass_print_textbox($args['setting'], $value); +} + +function _kompass_limit_logins_settings_radio_callback($args) +{ + kompass_print_radio($args['setting']); +} +function _kompass_limit_logins_settings_checkbox_callback($args) { + kompass_print_checkbox($args['setting']); +} + + + + + + +add_settings_section( + 'custom_settings_section', + 'Optionen', + 'custom_settings_section_callback', + BDP_LV_PLUGIN_SLUG . '-limit-login-attempts' +); + +$settings_page = BDP_LV_PLUGIN_SLUG . '-limit-login-attempts'; + + + +add_settings_field( + 'kompass_lla_1', + 'Maximale Wiederholungen', + '_kompass_limit_logins_settings_callback', + $settings_page, + 'custom_settings_section', + ['setting' => 'kompass_limit_login_allowed_retries']); + +add_settings_field( + 'kompass_lla_2', + 'Dauer der Sperre (in Minuten)', + '_kompass_limit_logins_settings_callback', + $settings_page, + 'custom_settings_section', + ['setting' => 'kompass_limit_login_lockout_duration', 'unit_division' => 60 ]); + +add_settings_field( + 'kompass_lla_3', + 'Maximale Anzahl an Sperrungen', + '_kompass_limit_logins_settings_callback', + $settings_page, + 'custom_settings_section', + ['setting' => 'kompass_limit_login_allowed_lockouts']); + +add_settings_field( + 'kompass_lla_4', + 'Langzeitsperre in Stunden', + '_kompass_limit_logins_settings_callback', + $settings_page, + 'custom_settings_section', + ['setting' => 'kompass_limit_login_long_duration', 'unit_division' => 3600]); + +add_settings_field( + 'kompass_lla_5', + 'Mininmale Passwort-Stärke:', + '_kompass_limit_logins_settings_radio_callback', + $settings_page, + 'custom_settings_section', + ['setting' => 'kompass_password_minimal_strength']); + +add_settings_field( + 'kompass_lla_6', + 'Seite erreichbar über:', + '_kompass_limit_logins_settings_radio_callback', + $settings_page, + 'custom_settings_section', + ['setting' => 'kompass_limit_login_client_type']); + +add_settings_field( + 'kompass_lla_7', + 'Cookies verarbeiten', + '_kompass_limit_logins_settings_radio_callback', + $settings_page, + 'custom_settings_section', + ['setting' => 'kompass_limit_login_cookies']); + +add_settings_field( + 'kompass_lla_8', + 'Bei Sperrung benachrichtigen', + '_kompass_limit_logins_settings_checkbox_callback', + $settings_page, + 'custom_settings_section', + ['setting' => 'kompass_limit_login_lockout_notify']); + +add_settings_field( + 'kompass_lla_9', + 'Fehlversuche bis zur Benachrichtigung', + '_kompass_limit_logins_settings_callback', + $settings_page, + 'custom_settings_section', + ['setting' => 'kompass_limit_login_notify_email_after']); diff --git a/modules/LimitLoginAttempts/includes/validators.php b/modules/LimitLoginAttempts/includes/validators.php new file mode 100644 index 0000000..e2fb154 --- /dev/null +++ b/modules/LimitLoginAttempts/includes/validators.php @@ -0,0 +1,64 @@ + 'short, bad, good, strong', + '2' => 'good, strong', + '3' => 'strong']; + + return ' ' . $possibleStrengths[$minPasswordStrength]; +} \ No newline at end of file