Plugin Directory

Changeset 3356074


Ignore:
Timestamp:
09/04/2025 12:34:07 PM (6 months ago)
Author:
axanet
Message:

Login security improvements

Location:
axanet-tools/trunk
Files:
3 edited

Legend:

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

    r3355661 r3356074  
    55Tested up to: 6.8
    66Requires PHP: 8.0
    7 Stable tag: 1.1.0
     7Stable tag: 1.1.1
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    4242== Changelog ==
    4343
     44= 1.1.1 =
     45* Fixed: Login security improvements
     46
    4447= 1.1.0 =
    4548* Added: Database Cleanup
     
    5255= 1.0.0 =
    5356* Initial release with features for disabling comments, customizing login logo and login security, disabling WP pages, managing admin bar, search & replace and maintenance mode.
    54 
    55 == Upgrade Notice ==
    56 = 1.0.0 =
    57 First release of the plugin.
  • axanet-tools/trunk/axanet-tools.php

    r3355661 r3356074  
    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.0
     13Version: 1.1.1
    1414Author: axanet GmbH
    1515Author URI: https://axanet.ch
     
    2424if ( ! defined( 'ABSPATH' ) ) exit;
    2525
    26 define( 'AXANET_TOOLS_VERSION', '1.1.0' );
     26define( 'AXANET_TOOLS_VERSION', '1.1.1' );
    2727define( 'AXANET_TOOLS_PATH', plugin_dir_path( __FILE__ ) );
    2828define( 'AXANET_TOOLS_URL', plugin_dir_url( __FILE__ ) );
  • axanet-tools/trunk/includes/login-security.php

    r3354211 r3356074  
    22/**
    33 * Login Security for axanet Tools
     4 *
     5 * Protects against brute-force attempts on wp-login, XML-RPC, and REST API.
    46 *
    57 * @package axanet-tools
     
    3133
    3234/**
    33  * Handle failed login
    34  */
    35 function axanet_login_security_handle_failed( $username ) {
     35 * Get user IP
     36 */
     37function 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;
     42}
     43
     44/**
     45 * Increment failed attempts for an IP and block if necessary
     46 */
     47function axanet_login_security_register_failed_attempt( $username = '' ) {
    3648    if ( ! axanet_login_security_is_enabled() ) {
    3749        return;
     
    6476    }
    6577}
    66 add_action( 'wp_login_failed', 'axanet_login_security_handle_failed' );
    67 
    68 /**
    69  * Prevent login if blocked
    70  */
    71 function axanet_login_security_block_check( $user ) {
     78
     79/**
     80 * Check if IP is blocked
     81 */
     82function axanet_login_security_is_ip_blocked() {
    7283    if ( ! axanet_login_security_is_enabled() ) {
    73         return $user;
     84        return false;
    7485    }
    7586
    7687    $ip = axanet_login_security_get_ip();
    7788    if ( ! $ip ) {
    78         return $user;
     89        return false;
    7990    }
    8091
    8192    $block_key = 'axanet_blocked_' . md5( $ip );
    82     if ( get_transient( $block_key ) ) {
     93    return (bool) get_transient( $block_key );
     94}
     95
     96/**
     97 * Handle failed login (wp-login)
     98 */
     99function axanet_login_security_wp_login_failed( $username ) {
     100    axanet_login_security_register_failed_attempt( $username );
     101}
     102add_action( 'wp_login_failed', 'axanet_login_security_wp_login_failed' );
     103
     104/**
     105 * Prevent login if blocked (wp-login)
     106 */
     107function axanet_login_security_authenticate( $user ) {
     108    if ( axanet_login_security_is_ip_blocked() ) {
    83109        return new WP_Error(
    84110            'axanet_blocked',
     
    86112        );
    87113    }
    88 
    89114    return $user;
    90115}
    91 add_filter( 'authenticate', 'axanet_login_security_block_check', 30 );
    92 
    93 /**
    94  * Get user IP
    95  */
    96 function axanet_login_security_get_ip() {
    97     if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
    98         return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
    99     }
    100     return false;
    101 }
     116add_filter( 'authenticate', 'axanet_login_security_authenticate', 30 );
     117
     118/**
     119 * Handle failed XML-RPC login attempts
     120 */
     121function axanet_login_security_xmlrpc_failed( $error ) {
     122    if ( isset( $_SERVER['PHP_AUTH_USER'] ) ) {
     123        $username = sanitize_text_field( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) );
     124        axanet_login_security_register_failed_attempt( $username );
     125    }
     126    return $error;
     127}
     128add_filter( 'xmlrpc_login_error', 'axanet_login_security_xmlrpc_failed' );
     129
     130/**
     131 * Block XML-RPC logins from blocked IPs
     132 */
     133function axanet_login_security_block_xmlrpc() {
     134    if ( axanet_login_security_is_ip_blocked() ) {
     135        wp_die(
     136            __( 'Too many failed login attempts. Please try again later.', 'axanet-tools' ),
     137            __( 'Blocked', 'axanet-tools' ),
     138            [ 'response' => 403 ]
     139        );
     140    }
     141}
     142add_action( 'xmlrpc_call', 'axanet_login_security_block_xmlrpc', 0 );
     143
     144/**
     145 * Block REST API logins from blocked IPs
     146 */
     147function axanet_login_security_block_rest( $user, $username, $password ) {
     148    if ( axanet_login_security_is_ip_blocked() ) {
     149        return new WP_Error(
     150            'axanet_blocked',
     151            __( 'Too many failed login attempts. Please try again later.', 'axanet-tools' ),
     152            [ 'status' => 403 ]
     153        );
     154    }
     155    return $user;
     156}
     157add_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    }
     169
     170    return $result;
     171});
     172
     173/**
     174 * Handle failed REST API login attempts
     175 */
     176add_filter( 'determine_current_user', function( $user_id ) {
     177    if ( ! axanet_login_security_is_enabled() ) {
     178        return $user_id;
     179    }
     180
     181    // If REST login attempt failed, register it
     182    if ( defined( 'REST_REQUEST' ) && REST_REQUEST && empty( $user_id ) ) {
     183        $username = sanitize_text_field( wp_unslash( $_REQUEST['username'] ?? '' ) );
     184        if ( $username ) {
     185            axanet_login_security_register_failed_attempt( $username );
     186        }
     187    }
     188
     189    return $user_id;
     190}, 100 );
    102191
    103192/**
     
    109198    }
    110199
    111     $error_message = '';
    112200    $is_enabled = axanet_login_security_is_enabled();
    113201
     
    117205
    118206        if ( $action === 'enable' ) {
    119             if ( update_option( 'axanet_login_security_enabled', 1 ) ) {
    120                 $is_enabled = true;
    121                 echo '<div class="notice notice-success"><p>' . esc_html__( 'Login Security enabled.', 'axanet-tools' ) . '</p></div>';
    122             }
     207            update_option( 'axanet_login_security_enabled', 1 );
     208            $is_enabled = true;
     209            echo '<div class="notice notice-success"><p>' . esc_html__( 'Login Security enabled.', 'axanet-tools' ) . '</p></div>';
    123210        } elseif ( $action === 'disable' ) {
    124             if ( update_option( 'axanet_login_security_enabled', 0 ) ) {
    125                 $is_enabled = false;
    126                 echo '<div class="notice notice-success"><p>' . esc_html__( 'Login Security disabled.', 'axanet-tools' ) . '</p></div>';
    127             }
     211            update_option( 'axanet_login_security_enabled', 0 );
     212            $is_enabled = false;
     213            echo '<div class="notice notice-success"><p>' . esc_html__( 'Login Security disabled.', 'axanet-tools' ) . '</p></div>';
    128214        }
    129215    }
     
    140226    }
    141227
    142     // Handle unlock
     228    // Handle IP unlock
    143229    if ( isset( $_POST['axanet_unlock_ip'] ) && check_admin_referer( 'axanet_login_security_unlock' ) ) {
    144230        $ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : '';
     
    149235                update_option( 'axanet_blocked_ips', $blocked );
    150236                delete_transient( 'axanet_blocked_' . md5( $ip ) );
    151    
    152                 /* translators: %s: The IP address that was unblocked. */
    153237                echo '<div class="notice notice-success"><p>' . esc_html( sprintf( esc_html__( 'IP %s has been unblocked.', 'axanet-tools' ), $ip ) ) . '</p></div>';
    154238            }
     
    156240    }
    157241
    158 
    159242    $settings = axanet_login_security_get_settings();
    160243    $blocked  = get_option( 'axanet_blocked_ips', [] );
     244
    161245    ?>
    162246    <div class="wrap">
     
    174258                    </p>
    175259                    <?php if ( $is_enabled ) : ?>
    176                         <button type="submit" name="login_security_action" value="disable" class="axanet-button axanet-disable" aria-label="<?php esc_attr_e( 'Disable Login Security', 'axanet-tools' ); ?>" data-confirm="<?php esc_attr_e( 'Are you sure you want to disable login security?', 'axanet-tools' ); ?>">
     260                        <button type="submit" name="login_security_action" value="disable" class="axanet-button axanet-disable" aria-label="<?php esc_attr_e( 'Disable Login Security', 'axanet-tools' ); ?>">
    177261                            <?php esc_html_e( 'Disable', 'axanet-tools' ); ?>
    178262                        </button>
Note: See TracChangeset for help on using the changeset viewer.