limit-login-attempts-reloaded
Last commit date
assets
1 month ago
core
1 month ago
languages
1 month ago
lib
1 month ago
resources
1 month ago
views
1 month ago
autoload.php
1 month ago
changelog.txt
1 month ago
limit-login-attempts-reloaded.php
1 month ago
readme.txt
1 month ago
limit-login-attempts-reloaded.php
318 lines
| 1 | <?php |
| 2 | /* |
| 3 | Plugin Name: Limit Login Attempts Reloaded |
| 4 | Description: Block excessive login attempts and protect your site against brute force attacks. Simple, yet powerful tools to improve site performance. |
| 5 | Author: Limit Login Attempts Reloaded |
| 6 | Author URI: https://www.limitloginattempts.com/ |
| 7 | Text Domain: limit-login-attempts-reloaded |
| 8 | Version: 3.2.4 |
| 9 | |
| 10 | Copyright 2008-2012 Johan Eenfeldt, 2016–present Limit Login Attempts Reloaded |
| 11 | */ |
| 12 | |
| 13 | if ( !defined( 'ABSPATH' ) ) { |
| 14 | exit; |
| 15 | } |
| 16 | |
| 17 | /*************************************************************************************** |
| 18 | * Constants |
| 19 | **************************************************************************************/ |
| 20 | define( 'LLA_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); |
| 21 | define( 'LLA_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); |
| 22 | define( 'LLA_PLUGIN_FILE', __FILE__ ); |
| 23 | define( 'LLA_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); |
| 24 | |
| 25 | /** |
| 26 | * Default risk widget config (bounds, colors, level rules). |
| 27 | * |
| 28 | * @return array |
| 29 | */ |
| 30 | function llar_get_risk_config_defaults() { |
| 31 | return array( |
| 32 | 'bounds' => array( |
| 33 | 'low_upper' => 100, |
| 34 | 'medium_upper' => 300, |
| 35 | ), |
| 36 | 'colors' => array( |
| 37 | 'green' => '#97F6C8', |
| 38 | 'yellow' => '#FFE066', |
| 39 | 'orange' => '#FFA34C', |
| 40 | 'red' => '#FF6633', |
| 41 | ), |
| 42 | 'levels' => array( |
| 43 | 'local' => array( |
| 44 | array( |
| 45 | 'exact' => 0, |
| 46 | 'title' => 'zero_title', |
| 47 | 'color' => 'green', |
| 48 | ), |
| 49 | array( |
| 50 | 'max_exclusive' => 100, |
| 51 | 'count_title' => true, |
| 52 | 'desc' => 'desc_low', |
| 53 | 'color' => 'yellow', |
| 54 | ), |
| 55 | array( |
| 56 | 'max_exclusive' => 300, |
| 57 | 'count_title' => true, |
| 58 | 'desc' => 'desc_medium', |
| 59 | /* Same recommendation block as high (red); threshold moved to 300+. */ |
| 60 | 'recommendation' => true, |
| 61 | 'color' => 'orange', |
| 62 | ), |
| 63 | array( |
| 64 | 'min_inclusive' => 300, |
| 65 | 'default' => true, |
| 66 | 'warning_title' => true, |
| 67 | 'recommendation' => true, |
| 68 | 'color' => 'red', |
| 69 | ), |
| 70 | ), |
| 71 | ), |
| 72 | ); |
| 73 | } |
| 74 | |
| 75 | /** |
| 76 | * Merge filtered config with defaults so colors/levels/bounds always exist. |
| 77 | * |
| 78 | * @param array $defaults Default config. |
| 79 | * @param mixed $cfg Filtered value. |
| 80 | * |
| 81 | * @return array |
| 82 | */ |
| 83 | function llar_normalize_risk_config( $defaults, $cfg ) { |
| 84 | if ( ! is_array( $cfg ) ) { |
| 85 | return $defaults; |
| 86 | } |
| 87 | |
| 88 | $out = $cfg; |
| 89 | foreach ( array( 'bounds', 'colors', 'levels' ) as $key ) { |
| 90 | if ( ! isset( $out[ $key ] ) || ! is_array( $out[ $key ] ) ) { |
| 91 | $out[ $key ] = $defaults[ $key ]; |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | if ( ! isset( $out['levels']['local'] ) || ! is_array( $out['levels']['local'] ) ) { |
| 96 | $out['levels']['local'] = $defaults['levels']['local']; |
| 97 | } |
| 98 | |
| 99 | return $out; |
| 100 | } |
| 101 | |
| 102 | /** |
| 103 | * Risk widget config (colors, level rules). Cached per request; overridable via llar_risk_config filter. |
| 104 | * |
| 105 | * @return array |
| 106 | */ |
| 107 | function llar_get_risk_config() { |
| 108 | static $cached = null; |
| 109 | |
| 110 | if ( null !== $cached ) { |
| 111 | return $cached; |
| 112 | } |
| 113 | |
| 114 | $defaults = llar_get_risk_config_defaults(); |
| 115 | $merged = apply_filters( 'llar_risk_config', $defaults ); |
| 116 | $cached = llar_normalize_risk_config( $defaults, $merged ); |
| 117 | |
| 118 | return $cached; |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * Warm risk config on init (after translations load). |
| 123 | * |
| 124 | * @return void |
| 125 | */ |
| 126 | function llar_define_risk_config() { |
| 127 | llar_get_risk_config(); |
| 128 | } |
| 129 | |
| 130 | add_action( 'init', 'llar_define_risk_config', 1 ); |
| 131 | |
| 132 | /*************************************************************************************** |
| 133 | * Different ways to get remote address: direct & behind proxy |
| 134 | **************************************************************************************/ |
| 135 | define( 'LLA_DIRECT_ADDR', 'REMOTE_ADDR' ); |
| 136 | define( 'LLA_PROXY_ADDR', 'HTTP_X_FORWARDED_FOR' ); |
| 137 | |
| 138 | /* Notify value checked against these in limit_login_sanitize_variables() */ |
| 139 | define( 'LLA_LOCKOUT_NOTIFY_ALLOWED', 'log,email' ); |
| 140 | |
| 141 | /** Regex: valid email for obfuscation (1=first, 2=middle, 3=last, 4=domain). */ |
| 142 | define( 'LLA_EMAIL_OBFUSCATE_REGEX', '/^(.)([^@]*)(.?)@(.*)$/' ); |
| 143 | /** Regex: one char in local part to mask (not first, not last). (?<=.) = at least one char before; [^@*] avoids re-matching asterisks. */ |
| 144 | define( 'LLA_EMAIL_OBFUSCATE_LOCAL', '/(?<=.)[^@*](?=[^@]+@)/' ); |
| 145 | /** Regex: one char in domain to mask (non-dot). */ |
| 146 | define( 'LLA_EMAIL_OBFUSCATE_DOMAIN', '/(?<=^[^@]*@.*)[^.]/' ); |
| 147 | |
| 148 | /*************************************************************************************** |
| 149 | * MFA constants (rescue codes, rate limiting, transients). |
| 150 | * Overridable: define in wp-config.php before plugin load to override defaults. |
| 151 | **************************************************************************************/ |
| 152 | defined( 'LLA_MFA_CODE_LENGTH' ) || define( 'LLA_MFA_CODE_LENGTH', 64 ); |
| 153 | defined( 'LLA_MFA_RESCUE_TOKEN_LENGTH' ) || define( 'LLA_MFA_RESCUE_TOKEN_LENGTH', 32 ); |
| 154 | defined( 'LLA_MFA_CODE_COUNT' ) || define( 'LLA_MFA_CODE_COUNT', 10 ); |
| 155 | /* Rescue link payload storage TTL (WordPress transients). Default 10 years; links are one-time (payload deleted on use). RESCUE_NOTICE_THRESHOLD is for admin warning; with a long TTL, "near expiry" is rare and missing/invalid payloads is the main trigger. */ |
| 156 | defined( 'LLA_MFA_RESCUE_LINK_TTL' ) || define( 'LLA_MFA_RESCUE_LINK_TTL', 10 * YEAR_IN_SECONDS ); |
| 157 | defined( 'LLA_MFA_RESCUE_NOTICE_THRESHOLD' ) || define( 'LLA_MFA_RESCUE_NOTICE_THRESHOLD', 5 * DAY_IN_SECONDS ); |
| 158 | defined( 'LLA_MFA_DISABLE_DURATION' ) || define( 'LLA_MFA_DISABLE_DURATION', 3600 ); |
| 159 | defined( 'LLA_MFA_RATE_LIMIT_PERIOD' ) || define( 'LLA_MFA_RATE_LIMIT_PERIOD', 3600 ); |
| 160 | defined( 'LLA_MFA_RESCUE_USE_COOLDOWN' ) || define( 'LLA_MFA_RESCUE_USE_COOLDOWN', 60 ); |
| 161 | defined( 'LLA_MFA_TRANSIENT_RESCUE_PREFIX' ) || define( 'LLA_MFA_TRANSIENT_RESCUE_PREFIX', 'llar_mfa_rescue_' ); |
| 162 | defined( 'LLA_MFA_TRANSIENT_RESCUE_LAST_USE' ) || define( 'LLA_MFA_TRANSIENT_RESCUE_LAST_USE', 'llar_rescue_last_use' ); |
| 163 | defined( 'LLA_MFA_TRANSIENT_MFA_DISABLED' ) || define( 'LLA_MFA_TRANSIENT_MFA_DISABLED', 'llar_mfa_temporarily_disabled' ); |
| 164 | defined( 'LLA_MFA_TRANSIENT_CHECKBOX_STATE' ) || define( 'LLA_MFA_TRANSIENT_CHECKBOX_STATE', 'llar_mfa_checkbox_state' ); |
| 165 | defined( 'LLA_MFA_CHECKBOX_STATE_TTL' ) || define( 'LLA_MFA_CHECKBOX_STATE_TTL', 300 ); |
| 166 | defined( 'LLA_MFA_PDF_RATE_LIMIT_MAX' ) || define( 'LLA_MFA_PDF_RATE_LIMIT_MAX', 5 ); |
| 167 | defined( 'LLA_MFA_PDF_RATE_LIMIT_PERIOD' ) || define( 'LLA_MFA_PDF_RATE_LIMIT_PERIOD', 60 ); |
| 168 | defined( 'LLA_MFA_WP_SALT_SCHEME_FALLBACK' ) || define( 'LLA_MFA_WP_SALT_SCHEME_FALLBACK', 'auth' ); |
| 169 | defined( 'LLA_MFA_BLOCK_REASON_SSL' ) || define( 'LLA_MFA_BLOCK_REASON_SSL', 'ssl' ); |
| 170 | defined( 'LLA_MFA_BLOCK_REASON_SALT' ) || define( 'LLA_MFA_BLOCK_REASON_SALT', 'salt' ); |
| 171 | defined( 'LLA_MFA_BLOCK_REASON_OPENSSL' ) || define( 'LLA_MFA_BLOCK_REASON_OPENSSL', 'openssl' ); |
| 172 | |
| 173 | /** MFA Flow: session and OTP transients (after failed login handshake). */ |
| 174 | defined( 'LLA_MFA_FLOW_TRANSIENT_SESSION_PREFIX' ) || define( 'LLA_MFA_FLOW_TRANSIENT_SESSION_PREFIX', 'llar_mfa_session_' ); |
| 175 | defined( 'LLA_MFA_FLOW_TRANSIENT_OTP_PREFIX' ) || define( 'LLA_MFA_FLOW_TRANSIENT_OTP_PREFIX', 'llar_mfa_otp_' ); |
| 176 | defined( 'LLA_MFA_FLOW_TRANSIENT_SEND_SECRET_PREFIX' ) || define( 'LLA_MFA_FLOW_TRANSIENT_SEND_SECRET_PREFIX', 'llar_mfa_send_secret_' ); |
| 177 | defined( 'LLA_MFA_FLOW_TRANSIENT_STATE_PREFIX' ) || define( 'LLA_MFA_FLOW_TRANSIENT_STATE_PREFIX', 'llar_mfa_state_' ); |
| 178 | defined( 'LLA_MFA_FLOW_OTP_TTL' ) || define( 'LLA_MFA_FLOW_OTP_TTL', 180 ); |
| 179 | defined( 'LLA_MFA_FLOW_HANDSHAKE_RATE_LIMIT_PERIOD' ) || define( 'LLA_MFA_FLOW_HANDSHAKE_RATE_LIMIT_PERIOD', 60 ); |
| 180 | defined( 'LLA_MFA_FLOW_HANDSHAKE_RATE_LIMIT_MAX' ) || define( 'LLA_MFA_FLOW_HANDSHAKE_RATE_LIMIT_MAX', 5 ); |
| 181 | defined( 'LLA_MFA_FLOW_LOG_PREFIX' ) || define( 'LLA_MFA_FLOW_LOG_PREFIX', 'LLAR MFA Flow: ' ); |
| 182 | /* POST field name for confirming a suspected-prefetch rescue request (value 1 + WP nonce). */ |
| 183 | defined( 'LLA_MFA_RESCUE_PREFETCH_BYPASS_ARG' ) || define( 'LLA_MFA_RESCUE_PREFETCH_BYPASS_ARG', 'llar_rescue_confirm' ); |
| 184 | |
| 185 | /** MFA Flow: API and session (values from constants, no UI settings). */ |
| 186 | defined( 'LLA_MFA_API_BASE_URL' ) || define( 'LLA_MFA_API_BASE_URL', 'https://api.limitloginattempts.com' ); |
| 187 | defined( 'LLA_MFA_API_PATH' ) || define( 'LLA_MFA_API_PATH', '/mfa' ); |
| 188 | defined( 'LLA_MFA_SESSION_TTL' ) || define( 'LLA_MFA_SESSION_TTL', 600 ); /* seconds, 10 minutes */ |
| 189 | defined( 'LLA_MFA_PROVIDER' ) || define( 'LLA_MFA_PROVIDER', 'llar' ); |
| 190 | |
| 191 | $um_limit_login_failed = false; |
| 192 | $limit_login_my_error_shown = false; /* have we shown our stuff? */ |
| 193 | $limit_login_just_lockedout = false; /* started this pageload??? */ |
| 194 | $limit_login_nonempty_credentials = false; /* user and pwd nonempty */ |
| 195 | |
| 196 | if ( file_exists( LLA_PLUGIN_DIR . 'autoload.php' ) ) { |
| 197 | |
| 198 | require_once LLA_PLUGIN_DIR . 'autoload.php'; |
| 199 | |
| 200 | add_action( |
| 201 | 'plugins_loaded', |
| 202 | function () { |
| 203 | ( new LLAR\Core\LimitLoginAttempts() ); |
| 204 | }, |
| 205 | 9999 |
| 206 | ); |
| 207 | |
| 208 | /** |
| 209 | * Activation hook: Cleanup old cron events and transients |
| 210 | */ |
| 211 | register_activation_hook( __FILE__, 'llar_mfa_activation_cleanup' ); |
| 212 | |
| 213 | function llar_mfa_activation_cleanup() { |
| 214 | // Clear old rescue transients |
| 215 | llar_mfa_cleanup_rescue_transients(); |
| 216 | |
| 217 | // Schedule daily cleanup if not already scheduled |
| 218 | if ( ! wp_next_scheduled( 'llar_mfa_daily_cleanup' ) ) { |
| 219 | wp_schedule_event( time(), 'daily', 'llar_mfa_daily_cleanup' ); |
| 220 | } |
| 221 | |
| 222 | if ( class_exists( 'LLAR\\Core\\Helpers' ) ) { |
| 223 | \LLAR\Core\Helpers::persist_stored_plugin_version(); |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | /** |
| 228 | * Deactivation hook: Cleanup cron events and transients (CRITICAL) |
| 229 | */ |
| 230 | register_deactivation_hook( __FILE__, 'llar_mfa_deactivation_cleanup' ); |
| 231 | |
| 232 | function llar_mfa_deactivation_cleanup() { |
| 233 | // Clear all scheduled events |
| 234 | wp_clear_scheduled_hook( 'llar_mfa_daily_cleanup' ); |
| 235 | |
| 236 | // Clear all rescue transients |
| 237 | llar_mfa_cleanup_rescue_transients(); |
| 238 | } |
| 239 | |
| 240 | /** |
| 241 | * Daily cleanup: Remove old transients (prevents DB accumulation) |
| 242 | */ |
| 243 | add_action( 'llar_mfa_daily_cleanup', 'llar_mfa_daily_cleanup' ); |
| 244 | |
| 245 | function llar_mfa_daily_cleanup() { |
| 246 | $keys = llar_mfa_get_expired_rescue_transient_keys(); |
| 247 | foreach ( $keys as $key ) { |
| 248 | delete_transient( $key ); |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | /** |
| 253 | * Get transient keys for rescue transients that are older than 1 day. |
| 254 | * Uses _transient_timeout_* where option_value is the expiration timestamp. |
| 255 | * |
| 256 | * @return array List of transient keys (e.g. llar_mfa_rescue_xxx). |
| 257 | */ |
| 258 | function llar_mfa_get_expired_rescue_transient_keys() { |
| 259 | global $wpdb; |
| 260 | $prefix = LLA_MFA_TRANSIENT_RESCUE_PREFIX; |
| 261 | $cutoff = time() - DAY_IN_SECONDS; |
| 262 | $like = $wpdb->esc_like( '_transient_timeout_' . $prefix ) . '%'; |
| 263 | $names = $wpdb->get_col( |
| 264 | $wpdb->prepare( |
| 265 | 'SELECT option_name FROM ' . $wpdb->options . ' WHERE option_name LIKE %s AND option_value < %d', |
| 266 | $like, |
| 267 | $cutoff |
| 268 | ) |
| 269 | ); |
| 270 | if ( ! is_array( $names ) ) { |
| 271 | return array(); |
| 272 | } |
| 273 | $prefix_len = strlen( '_transient_timeout_' ); |
| 274 | $keys = array(); |
| 275 | foreach ( $names as $name ) { |
| 276 | $keys[] = substr( $name, $prefix_len ); |
| 277 | } |
| 278 | return $keys; |
| 279 | } |
| 280 | |
| 281 | /** |
| 282 | * Helper: delete all rescue transients (e.g. on deactivation). |
| 283 | * Uses delete_transient() so object cache stays in sync. |
| 284 | */ |
| 285 | function llar_mfa_cleanup_rescue_transients() { |
| 286 | $keys = llar_mfa_get_all_rescue_transient_keys(); |
| 287 | foreach ( $keys as $key ) { |
| 288 | delete_transient( $key ); |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | /** |
| 293 | * Get all rescue transient keys (for full cleanup). |
| 294 | * |
| 295 | * @return array List of transient keys. |
| 296 | */ |
| 297 | function llar_mfa_get_all_rescue_transient_keys() { |
| 298 | global $wpdb; |
| 299 | $prefix = LLA_MFA_TRANSIENT_RESCUE_PREFIX; |
| 300 | $like = $wpdb->esc_like( '_transient_timeout_' . $prefix ) . '%'; |
| 301 | $names = $wpdb->get_col( |
| 302 | $wpdb->prepare( |
| 303 | 'SELECT option_name FROM ' . $wpdb->options . ' WHERE option_name LIKE %s', |
| 304 | $like |
| 305 | ) |
| 306 | ); |
| 307 | if ( ! is_array( $names ) ) { |
| 308 | return array(); |
| 309 | } |
| 310 | $prefix_len = strlen( '_transient_timeout_' ); |
| 311 | $keys = array(); |
| 312 | foreach ( $names as $name ) { |
| 313 | $keys[] = substr( $name, $prefix_len ); |
| 314 | } |
| 315 | return $keys; |
| 316 | } |
| 317 | } |
| 318 |