Plugin Directory

Changeset 3356220


Ignore:
Timestamp:
09/04/2025 04:17:05 PM (6 months ago)
Author:
axanet
Message:

Fixed: Login security improvements
Version update 1.1.2

Location:
axanet-tools/trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • axanet-tools/trunk/README.md

    r3356074 r3356220  
    55Tested up to: 6.8
    66Requires PHP: 8.0
    7 Stable tag: 1.1.1
     7Stable tag: 1.1.2
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    4242== Changelog ==
    4343
     44= 1.1.2 =
     45* Fixed: Login security improvements
     46
    4447= 1.1.1 =
    4548* Fixed: Login security improvements
  • axanet-tools/trunk/axanet-tools.php

    r3356074 r3356220  
    1111Plugin Name: axanet Tools
    1212Description: Essential tools to edit login logo and login security, disable comments site-wide with optional deletion, disable system pages, manage admin bar visibility, search & replace database strings, clean up database and control maintenance mode.
    13 Version: 1.1.1
     13Version: 1.1.2
    1414Author: axanet GmbH
    1515Author URI: https://axanet.ch
     
    2424if ( ! defined( 'ABSPATH' ) ) exit;
    2525
    26 define( 'AXANET_TOOLS_VERSION', '1.1.1' );
     26define( 'AXANET_TOOLS_VERSION', '1.1.2' );
    2727define( 'AXANET_TOOLS_PATH', plugin_dir_path( __FILE__ ) );
    2828define( 'AXANET_TOOLS_URL', plugin_dir_url( __FILE__ ) );
  • axanet-tools/trunk/includes/login-security.php

    r3356074 r3356220  
    1010 */
    1111
    12 if ( ! defined( 'ABSPATH' ) ) exit;
     12if ( ! defined( 'ABSPATH' ) ) {
     13    exit;
     14}
    1315
    1416/**
     
    3335
    3436/**
    35  * Get user IP
     37 * Get user IP with validation
    3638 */
    3739function axanet_login_security_get_ip() {
    38     if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
    39         return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
    40     }
    41     return false;
     40    $ip = '';
     41
     42    $forwarded = filter_input( INPUT_SERVER, 'HTTP_X_FORWARDED_FOR', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
     43    if ( ! empty( $forwarded ) ) {
     44        $ip_chain = explode( ',', $forwarded );
     45        $ip       = trim( $ip_chain[0] );
     46    } else {
     47        $client_ip = filter_input( INPUT_SERVER, 'HTTP_CLIENT_IP', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
     48        $remote_ip = filter_input( INPUT_SERVER, 'REMOTE_ADDR', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
     49
     50        if ( ! empty( $client_ip ) ) {
     51            $ip = $client_ip;
     52        } elseif ( ! empty( $remote_ip ) ) {
     53            $ip = $remote_ip;
     54        }
     55    }
     56
     57    return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : false;
    4258}
    4359
     
    5672
    5773    $settings = axanet_login_security_get_settings();
    58     $key      = 'axanet_login_fail_' . md5( $ip );
     74    $key      = 'axanet_login_fail_' . preg_replace( '/[^a-zA-Z0-9_]/', '_', $ip );
    5975    $fails    = (int) get_transient( $key );
    6076
     
    6480    if ( $fails >= $settings['attempts'] ) {
    6581        // Block IP
    66         $block_key = 'axanet_blocked_' . md5( $ip );
     82        $block_key = 'axanet_blocked_' . preg_replace( '/[^a-zA-Z0-9_]/', '_', $ip );
    6783        set_transient( $block_key, true, $settings['block_time'] * MINUTE_IN_SECONDS );
    6884
    6985        // Save in blocked list for admin UI
    70         $blocked = get_option( 'axanet_blocked_ips', [] );
     86        $blocked        = get_option( 'axanet_blocked_ips', [] );
    7187        $blocked[ $ip ] = [
    7288            'blocked_until' => time() + ( $settings['block_time'] * MINUTE_IN_SECONDS ),
     
    7894
    7995/**
     96 * Clear failed attempts on successful login
     97 */
     98function axanet_login_security_clear_failed_attempts( $username ) {
     99    if ( ! axanet_login_security_is_enabled() ) {
     100        return;
     101    }
     102
     103    $ip = axanet_login_security_get_ip();
     104    if ( $ip ) {
     105        $key = 'axanet_login_fail_' . preg_replace( '/[^a-zA-Z0-9_]/', '_', $ip );
     106        delete_transient( $key );
     107    }
     108}
     109add_action( 'wp_login', 'axanet_login_security_clear_failed_attempts' );
     110
     111/**
    80112 * Check if IP is blocked
    81113 */
     
    90122    }
    91123
    92     $block_key = 'axanet_blocked_' . md5( $ip );
     124    $block_key = 'axanet_blocked_' . preg_replace( '/[^a-zA-Z0-9_]/', '_', $ip );
    93125    return (bool) get_transient( $block_key );
    94126}
     
    134166    if ( axanet_login_security_is_ip_blocked() ) {
    135167        wp_die(
    136             __( 'Too many failed login attempts. Please try again later.', 'axanet-tools' ),
    137             __( 'Blocked', 'axanet-tools' ),
     168            esc_html__( 'Too many failed login attempts. Please try again later.', 'axanet-tools' ),
     169            esc_html__( 'Blocked', 'axanet-tools' ),
    138170            [ 'response' => 403 ]
    139171        );
     
    145177 * Block REST API logins from blocked IPs
    146178 */
    147 function axanet_login_security_block_rest( $user, $username, $password ) {
     179function axanet_login_security_block_rest( $result ) {
     180    if ( ! empty( $result ) ) {
     181        return $result; // skip if already blocked
     182    }
     183
    148184    if ( axanet_login_security_is_ip_blocked() ) {
    149185        return new WP_Error(
     
    153189        );
    154190    }
    155     return $user;
    156 }
    157 add_filter( 'rest_authentication_errors', function( $result ) {
    158     if ( ! empty( $result ) ) {
    159         return $result; // skip if already blocked
    160     }
    161 
    162     if ( axanet_login_security_is_ip_blocked() ) {
    163         return new WP_Error(
    164             'axanet_blocked',
    165             __( 'Too many failed login attempts. Please try again later.', 'axanet-tools' ),
    166             [ 'status' => 403 ]
    167         );
    168     }
    169191
    170192    return $result;
    171 });
     193}
     194add_filter( 'rest_authentication_errors', 'axanet_login_security_block_rest' );
    172195
    173196/**
    174197 * Handle failed REST API login attempts
    175198 */
    176 add_filter( 'determine_current_user', function( $user_id ) {
     199function axanet_login_security_rest_failed( $user_id ) {
    177200    if ( ! axanet_login_security_is_enabled() ) {
    178201        return $user_id;
     
    181204    // If REST login attempt failed, register it
    182205    if ( defined( 'REST_REQUEST' ) && REST_REQUEST && empty( $user_id ) ) {
    183         $username = sanitize_text_field( wp_unslash( $_REQUEST['username'] ?? '' ) );
     206        $username = '';
     207
     208        if ( ! empty( $_SERVER['PHP_AUTH_USER'] ) ) {
     209            $username = sanitize_text_field( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) );
     210        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not applicable for REST API login requests.
     211        } elseif ( ! empty( $_REQUEST['username'] ) ) {
     212        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not applicable for REST API login requests.
     213            $username = sanitize_text_field( wp_unslash( $_REQUEST['username'] ) );
     214        }
     215
    184216        if ( $username ) {
    185217            axanet_login_security_register_failed_attempt( $username );
     
    188220
    189221    return $user_id;
    190 }, 100 );
     222}
     223add_filter( 'determine_current_user', 'axanet_login_security_rest_failed', 100 );
     224
     225/**
     226 * Cleanup expired blocked IPs from the list
     227 */
     228function axanet_login_security_cleanup_blocked_ips() {
     229    $blocked      = get_option( 'axanet_blocked_ips', [] );
     230    $current_time = time();
     231    $changed      = false;
     232
     233    foreach ( $blocked as $ip => $data ) {
     234        if ( $data['blocked_until'] < $current_time ) {
     235            unset( $blocked[ $ip ] );
     236            $changed = true;
     237        }
     238    }
     239
     240    if ( $changed ) {
     241        update_option( 'axanet_blocked_ips', $blocked );
     242    }
     243}
    191244
    192245/**
     
    198251    }
    199252
     253    // Cleanup expired blocked IPs on admin page load
     254    axanet_login_security_cleanup_blocked_ips();
     255
    200256    $is_enabled = axanet_login_security_is_enabled();
    201257
     
    204260        $action = isset( $_POST['login_security_action'] ) ? sanitize_text_field( wp_unslash( $_POST['login_security_action'] ) ) : '';
    205261
    206         if ( $action === 'enable' ) {
     262        if ( 'enable' === $action ) {
    207263            update_option( 'axanet_login_security_enabled', 1 );
    208264            $is_enabled = true;
    209265            echo '<div class="notice notice-success"><p>' . esc_html__( 'Login Security enabled.', 'axanet-tools' ) . '</p></div>';
    210         } elseif ( $action === 'disable' ) {
     266        } elseif ( 'disable' === $action ) {
    211267            update_option( 'axanet_login_security_enabled', 0 );
    212268            $is_enabled = false;
     
    229285    if ( isset( $_POST['axanet_unlock_ip'] ) && check_admin_referer( 'axanet_login_security_unlock' ) ) {
    230286        $ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : '';
    231         if ( $ip ) {
     287        if ( $ip && filter_var( $ip, FILTER_VALIDATE_IP ) ) {
    232288            $blocked = get_option( 'axanet_blocked_ips', [] );
    233289            if ( isset( $blocked[ $ip ] ) ) {
    234290                unset( $blocked[ $ip ] );
    235291                update_option( 'axanet_blocked_ips', $blocked );
    236                 delete_transient( 'axanet_blocked_' . md5( $ip ) );
    237                 echo '<div class="notice notice-success"><p>' . esc_html( sprintf( esc_html__( 'IP %s has been unblocked.', 'axanet-tools' ), $ip ) ) . '</p></div>';
     292                $block_key = 'axanet_blocked_' . preg_replace( '/[^a-zA-Z0-9_]/', '_', $ip );
     293                delete_transient( $block_key );
     294                /* translators: %s: IP address */
     295                echo '<div class="notice notice-success"><p>' . esc_html( sprintf( __( 'IP %s has been unblocked.', 'axanet-tools' ), $ip ) ) . '</p></div>';
    238296            }
    239297        }
Note: See TracChangeset for help on using the changeset viewer.