📄 Viewing: class-limit-login-attempts.php
<?php
namespace ASENHA\Classes;
use WP_Error;
/**
* Class for Limit Login Attempts module
*
* @since 6.9.5
*/
class Limit_Login_Attempts {
/**
* Maybe allow login if not locked out. Should return WP_Error object if not allowed to login.
*
* @since 2.5.0
*/
public function maybe_allow_login( $user_or_error, $username, $password ) {
global $wpdb, $asenha_limit_login;
$table_name = $wpdb->prefix . 'asenha_failed_logins';
// Maybe create table if it does not exist yet, e.g. upgraded from previous version of plugin, so, no activation methods are fired
$query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table_name ) );
if ( $wpdb->get_var( $query ) === $table_name ) {
// Table already exists, do nothing.
} else {
$activation = new Activation;
$activation->create_failed_logins_log_table();
}
// Get values from options needed to do various checks
$options = get_option( ASENHA_SLUG_U, array() );
$login_fails_allowed = $options['login_fails_allowed'];
$login_lockout_maxcount = $options['login_lockout_maxcount'];
$ip_address_whitelist_raw = ( isset( $options['limit_login_attempts_ip_whitelist'] ) ) ? explode( PHP_EOL, $options['limit_login_attempts_ip_whitelist'] ) : array();
$ip_address_whitelist = array();
if ( ! empty( $ip_address_whitelist_raw ) ) {
foreach( $ip_address_whitelist_raw as $ip_address ) {
$ip_address_whitelist[] = trim( $ip_address );
}
}
$change_login_url = $options['change_login_url'];
$custom_login_slug = $options['custom_login_slug'];
// Instantiate object to access common methods
$common_methods = new Common_Methods;
// Get user/visitor IP address
$ip_address = $common_methods->get_user_ip_address( 'ip', 'limit-login-attempts' );
if ( ! in_array( $ip_address, $ip_address_whitelist ) ) { // IP is not whitelisted
// Check if IP address has failed login attempts recorded in the DB log
$sql = $wpdb->prepare("SELECT * FROM `" . $table_name . "` Where `ip_address` = %s", $ip_address);
$result = $wpdb->get_results( $sql, ARRAY_A );
$result_count = count( $result );
if ( $result_count > 0 ) { // IP address has been recorded in the database.
$fail_count = $result[0]['fail_count'];
$lockout_count = $result[0]['lockout_count'];
$last_fail_on = $result[0]['unixtime'];
} else {
$fail_count = 0;
$lockout_count = 0;
$last_fail_on = '';
}
} else { // IP is whitelisted
$result = array();
$result_count = 0;
$fail_count = 0;
$lockout_count = 0;
$last_fail_on = '';
}
// Initialize the global variable
$asenha_limit_login = array (
'ip_address' => $ip_address,
'request_uri' => sanitize_text_field( $_SERVER['REQUEST_URI'] ),
'ip_address_log' => $result,
'fail_count' => $fail_count,
'lockout_count' => $lockout_count,
'maybe_lockout' => false,
'extended_lockout' => false,
'within_lockout_period' => false,
'lockout_period' => 0,
'lockout_period_remaining' => 0,
'login_fails_allowed' => $login_fails_allowed,
'login_lockout_maxcount' => $login_lockout_maxcount,
// 'default_lockout_period' => 15, // 15 seconds. FOR TESTING.
// 'default_lockout_period' => 60, // 1 minutes in seconds
'default_lockout_period' => 60*15, // 15 minutes in seconds
// 'extended_lockout_period' => 3*60, // 3 minutes in seconds
'extended_lockout_period' => 24*60*60, // 24 hours in seconds
'change_login_url' => $change_login_url, // is custom login URL enabled?
'custom_login_slug' => $custom_login_slug,
);
if ( ! in_array( $ip_address, $ip_address_whitelist ) ) { // IP is not whitelisted
if ( $result_count > 0 ) { // IP address has been recorded in the database.
// Failed attempts have been recorded and fulfills lockout condition
if ( ! empty( $fail_count ) && ( ( $fail_count ) % $login_fails_allowed == 0 ) ) {
$asenha_limit_login['maybe_lockout'] = true;
// Has reached max / gone beyond number of lockouts allowed?
if ( $lockout_count >= $login_lockout_maxcount ) {
$asenha_limit_login['extended_lockout'] = true;
$lockout_period = $asenha_limit_login['extended_lockout_period'];
} else {
$asenha_limit_login['extended_lockout'] = false;
$lockout_period = $asenha_limit_login['default_lockout_period'];
}
$asenha_limit_login['lockout_period'] = $lockout_period;
// User/visitor is still within the lockout period
if ( ( time() - $last_fail_on ) <= $asenha_limit_login['lockout_period'] ) {
$asenha_limit_login['within_lockout_period'] = true;
$asenha_limit_login['lockout_period_remaining'] = $asenha_limit_login['lockout_period'] - ( time() - $last_fail_on );
if ( $asenha_limit_login['lockout_period_remaining'] <= 60 ) {
// Get remaining lockout period in minutes and seconds
$lockout_period_remaining = $asenha_limit_login['lockout_period_remaining'] . ' seconds';
} elseif ( $asenha_limit_login['lockout_period_remaining'] <= 60*60 ) {
// Get remaining lockout period in minutes and seconds
$lockout_period_remaining = $common_methods->seconds_to_period( $asenha_limit_login['lockout_period_remaining'], 'to-minutes-seconds' );
} elseif ( $asenha_limit_login['lockout_period_remaining'] > 60*60 && $asenha_limit_login['lockout_period_remaining'] <= 24*60*60 ) {
// Get remaining lockout period in minutes and seconds
$lockout_period_remaining = $common_methods->seconds_to_period( $asenha_limit_login['lockout_period_remaining'], 'to-hours-minutes-seconds' );
} elseif ( $asenha_limit_login['lockout_period_remaining'] > 24*60*60 ) {
// Get remaining lockout period in minutes and seconds
$lockout_period_remaining = $common_methods->seconds_to_period( $asenha_limit_login['lockout_period_remaining'], 'to-days-hours-minutes-seconds' );
}
$error = new WP_Error( 'ip_address_blocked', '<b>WARNING:</b> You\'ve been locked out. You can login again in ' . $lockout_period_remaining . '.' );
return $error;
} else { // User/visitor is no longer within the lockout period
$asenha_limit_login['within_lockout_period'] = false;
if ( $lockout_count == $login_lockout_maxcount ) {
// Remove the DB log entry for the current IP address. i.e. release from extended lockout
$where = array( 'ip_address' => $ip_address );
$where_format = array( '%s' );
// Delete existing data in the database
$wpdb->delete(
$table_name,
$where,
$where_format
);
}
return $user_or_error;
}
} else {
$asenha_limit_login['maybe_lockout'] = false;
return $user_or_error;
}
} else { // IP address has not been recorded in the database.
return $user_or_error;
}
} else { // IP is whitelisted
return $user_or_error;
}
}
/**
* Handle login errors
*
* @link https://developer.wordpress.org/reference/classes/wp_error/#methods
* @since 2.5.0
*/
public function login_error_handler( $errors, $redirect_to ) {
global $asenha_limit_login;
if ( is_wp_error( $errors ) ) {
$error_codes = $errors->get_error_codes();
foreach ( $error_codes as $error_code ) {
if ( $error_code == 'invalid_username' || $error_code == 'incorrect_password' ) {
// Remove default error messages that may give out valueable info to hackers
$errors->remove( 'invalid_username' ); // Outputs info that says username does not exist. May encourage login attempt with a different username instead.
$errors->remove( 'incorrect_password' ); // Outputs info that implies username exist. May encourage login attempt with a different password.
// Add a new error message that does not provide useful clues to hackers
$errors->add( 'invalid_username_or_incorrect_password', '<b>' . __( 'Error:', 'admin-site-enhancements' ) . '</b> ' . __( 'Invalid username/email or incorrect password.', 'admin-site-enhancements' ) );
// $errors->add( 'another_error_code', 'The error message.' );
}
}
}
return $errors;
}
/**
* Disable login form inputs via CSS
*
* @since 2.5.0
*/
public function maybe_hide_login_form() {
global $asenha_limit_login;
if ( isset( $asenha_limit_login['within_lockout_period'] ) && $asenha_limit_login['within_lockout_period'] ) {
// Hide logo, login form and the links below it
?>
<script>
document.addEventListener("DOMContentLoaded", function(event) {
var loginForm = document.getElementById("loginform");
loginForm.remove();
});
</script>
<style type="text/css">
body.login {
background:#f6d6d7;
}
#login h1,
#loginform,
#login #nav,
#backtoblog,
.language-switcher {
display: none;
}
@media screen and (max-height: 550px) {
#login {
padding: 80px 0 20px !important;
}
}
</style>
<?php
} else {
$options = get_option( ASENHA_SLUG_U, array() );
$login_fails_allowed = $options['login_fails_allowed'];
$page_was_reloaded = isset( $_GET['rl'] ) && 1 == sanitize_text_field( $_GET['rl'] ) ? true : false;
if ( isset( $asenha_limit_login['fail_count'] )
&& ( ( $login_fails_allowed - 1 ) == intval( $asenha_limit_login['fail_count'] )
|| ( 2 * $login_fails_allowed - 1 ) == intval( $asenha_limit_login['fail_count'] )
|| ( 3 * $login_fails_allowed - 1 ) == intval( $asenha_limit_login['fail_count'] )
|| ( 4 * $login_fails_allowed - 1 ) == intval( $asenha_limit_login['fail_count'] )
|| ( 5 * $login_fails_allowed - 1 ) == intval( $asenha_limit_login['fail_count'] )
|| ( 6 * $login_fails_allowed - 1 ) == intval( $asenha_limit_login['fail_count'] )
)
) {
if ( array_key_exists( 'change_login_url', $options ) && $options['change_login_url'] ) {
// Custom Login URL is enabled, e.g. /manage
// Do nothing
} else {
// Default login URL, i.e. /wp-login.php
// Reload the login page so we get the up-to-date data in $asenha_limit_login
// Only reload if page was not reloaded before. This prevents infinite reloads.
if ( ! $page_was_reloaded ) {
?>
<script>
let url = window.location.href;
if (url.indexOf('?') > -1){
url += '&rl=1'
} else {
url += '?rl=1'
}
location.replace(url);
</script>
<?php
}
}
}
}
}
/**
* Add login error message on top of the login form
*
* @since 2.5.0
*/
public function add_failed_login_message( $message ) {
global $asenha_limit_login;
if ( isset( $_REQUEST['failed_login'] ) && $_REQUEST['failed_login'] == 'true' ) {
if ( ! is_null( $asenha_limit_login ) && isset( $asenha_limit_login['within_lockout_period'] ) && ! $asenha_limit_login['within_lockout_period'] ) {
$message = '<div id="login_error" class="notice notice-error"><b>' . __( 'Error:', 'admin-site-enhancements' ) . '</b> ' . __( 'Invalid username/email or incorrect password.', 'admin-site-enhancements' ) . '</div>';
}
}
return $message;
}
/**
* Log failed login attempts
*
* @since 2.5.0
*/
public function log_failed_login( $username ) {
global $wpdb, $asenha_limit_login;
$table_name = $wpdb->prefix . 'asenha_failed_logins';
$ip_address = isset( $asenha_limit_login['ip_address'] ) ? $asenha_limit_login['ip_address'] : '';
$request_uri = isset( $asenha_limit_login['request_uri'] ) ? $asenha_limit_login['request_uri'] : '';
$login_fails_allowed = isset( $asenha_limit_login['login_fails_allowed'] ) ? $asenha_limit_login['login_fails_allowed'] : 3;
$login_lockout_maxcount = isset( $asenha_limit_login['login_lockout_maxcount'] ) ? $asenha_limit_login['login_lockout_maxcount'] : 3;
// Check if the IP address has been used in a failed login attempt before, i.e. has it been recorded in the database?
$sql = $wpdb->prepare( "SELECT * FROM `" . $table_name . "` WHERE `ip_address` = %s", $ip_address );
$result = $wpdb->get_results( $sql, ARRAY_A );
if ( $result ) {
$result_count = count( $result );
} else {
$result_count = 0;
}
// Update logged info for the IP address in the global variable
if ( $result ) {
$asenha_limit_login['ip_address_log'] = $result;
}
if ( $result_count == 0 ) { // IP address has not been recorded in the database.
$new_fail_count = 1;
$new_lockout_count = 0;
} else { // IP address has been recorded in the database.
$new_fail_count = $result[0]['fail_count'] + 1;
$new_lockout_count = floor( ( $result[0]['fail_count'] + 1 ) / $login_fails_allowed );
}
// Get the URL where login failed, i.e. where brute force attack might be happening
// $login_url = ( ! empty( $_SERVER['HTTPS'] ) ? 'https://' : 'http://') . sanitize_text_field( $_SERVER['HTTP_HOST'] ) . sanitize_text_field( $_SERVER['REQUEST_URI'] );
// Time stamps
$unixtime = time();
if ( function_exists( 'wp_date' ) ) {
$datetime_wp = wp_date( 'Y-m-d H:i:s', $unixtime );
} else {
$datetime_wp = date_i18n( 'Y-m-d H:i:s', $unixtime );
}
$data = array(
'ip_address' => $ip_address,
'username' => $username,
'fail_count' => $new_fail_count,
'lockout_count' => $new_lockout_count,
'request_uri' => $request_uri,
'unixtime' => $unixtime,
'datetime_wp' => $datetime_wp,
'info' => '',
);
$data_format = array(
'%s', // string
'%s', // string
'%d', // integer
'%d', // integer
'%s', // string
'%d', // integer
'%s', // string
'%s', // string
);
if ( $result_count == 0 ) {
// Insert into the database
$result = $wpdb->insert(
$table_name,
$data,
$data_format
);
} else {
$fail_count = $result[0]['fail_count'];
$lockout_count = $result[0]['lockout_count'];
$last_fail_on = $result[0]['unixtime'];
$where = array( 'ip_address' => $ip_address );
$where_format = array( '%s' );
// Failed attempts have been recorded and fulfills lockout condition
if ( ! empty( $fail_count )
&& ( $login_fails_allowed > 0 )
&& ( $fail_count % $login_fails_allowed == 0 )
) {
// Has reached max / gone beyond number of lockouts allowed?
if ( $lockout_count >= $login_lockout_maxcount ) {
$asenha_limit_login['extended_lockout'] = true;
$lockout_period = $asenha_limit_login['extended_lockout_period'];
} else {
$asenha_limit_login['extended_lockout'] = false;
$lockout_period = $asenha_limit_login['default_lockout_period'];
}
$asenha_limit_login['lockout_period'] = $lockout_period;
// User/visitor is still within the lockout period
if ( ( time() - $last_fail_on ) <= $lockout_period ) {
// Do nothing
} else {
if ( $lockout_count < $login_lockout_maxcount ) {
// Update existing data in the database
$wpdb->update(
$table_name,
$data,
$where,
$data_format,
$where_format
);
}
}
} else {
// Update existing data in the database
$wpdb->update(
$table_name,
$data,
$where,
$data_format,
$where_format
);
}
}
}
/**
* Clear failed login attempts log after successful login
*
* @since 2.5.0
*/
public function clear_failed_login_log() {
global $wpdb, $asenha_limit_login;
$table_name = $wpdb->prefix . 'asenha_failed_logins';
$ip_address = isset( $asenha_limit_login['ip_address'] ) ? $asenha_limit_login['ip_address'] : '';
// Remove the DB log entry for the current IP address.
$where = array( 'ip_address' => $ip_address );
$where_format = array( '%s' );
$wpdb->delete(
$table_name,
$where,
$where_format
);
}
/**
* Trigger scheduling of email delivery log clean up event
*
* @since 7.1.1
*/
public function trigger_clear_or_schedule_log_clean_up_by_amount( $option_name ) {
if ( 'failed_login_attempts_log_schedule_cleanup_by_amount' == $option_name ) {
$this->clear_or_schedule_log_clean_up_by_amount();
}
}
/**
* Schedule failed login attempts log clean up event
*
* @link https://plugins.trac.wordpress.org/browser/lana-email-logger/tags/1.1.0/lana-email-logger.php#L750
* @since 7.8.3
*/
public function clear_or_schedule_log_clean_up_by_amount() {
$options = get_option( ASENHA_SLUG_U, array() );
$failed_login_attempts_log_schedule_cleanup_by_amount = isset( $options['failed_login_attempts_log_schedule_cleanup_by_amount'] ) ? $options['failed_login_attempts_log_schedule_cleanup_by_amount'] : false;
// If scheduled clean up is not enabled, let's clear the schedule
if ( ! $failed_login_attempts_log_schedule_cleanup_by_amount ) {
wp_clear_scheduled_hook( 'asenha_failed_login_attempts_log_cleanup_by_amount' );
return;
}
// If there's no next scheduled clean up event, let's schedule one
if ( ! wp_next_scheduled( 'asenha_failed_login_attempts_log_cleanup_by_amount' ) ) {
wp_schedule_event( time(), 'hourly', 'asenha_failed_login_attempts_log_cleanup_by_amount' );
}
}
/**
* Perform clean up of failed login attempts log by the amount of entries to keep
*
* @link https://plugins.trac.wordpress.org/browser/lana-email-logger/tags/1.1.0/lana-email-logger.php#L768
* @since 7.8.3
*/
public function perform_failed_login_attempts_log_clean_up_by_amount() {
global $wpdb;
$options = get_option( ASENHA_SLUG_U, array() );
$failed_login_attempts_log_schedule_cleanup_by_amount = isset( $options['failed_login_attempts_log_schedule_cleanup_by_amount'] ) ? $options['failed_login_attempts_log_schedule_cleanup_by_amount'] : false;
$failed_login_attempts_log_entries_amount_to_keep = 1000;
// Bail if scheduled clean up by amount is not enabled
if ( ! $failed_login_attempts_log_schedule_cleanup_by_amount ) {
return;
}
$table_name = $wpdb->prefix.'asenha_failed_logins';
$wpdb->query( "DELETE failed_login_entries FROM " . $table_name . "
AS failed_login_entries JOIN ( SELECT id FROM " . $table_name . " ORDER BY id DESC LIMIT 1 OFFSET " . $failed_login_attempts_log_entries_amount_to_keep . " )
AS failed_login_entries_limit ON failed_login_entries.id <= failed_login_entries_limit.id;" );
}
}
🌑 DarkStealth — WP Plugin Edition
Directory: /home/httpd/html/matrixmodels.com/public_html/wp-content/plugins/admin-site-enhancements/classes