Limit Login Attempts Reloaded - Version 2.8.0

Version Description

  • The plugin doesn't trust any IP addresses other than SERVER["REMOTE_ADDR"] anymore. Trusting other IP origins make protection useless b/c they can be easily faked. This new version provides a way of secure IP unlocking for those sites that use a reverse proxy coupled with misconfigurated servers that populate _SERVER["REMOTEADDR"] with wrong IPs which leads to mass blocking of users.
Download this release

Release Info

Developer wpchefgadget
Plugin Icon 128x128 Limit Login Attempts Reloaded
Version 2.8.0
Comparing to
See all releases

Code changes from version 2.7.4 to 2.8.0

core/LimitLoginAttempts.php CHANGED
@@ -63,6 +63,7 @@ class Limit_Login_Attempts
63
  public function hooks_init() {
64
  add_action( 'plugins_loaded', array( $this, 'setup' ), 9999 );
65
  add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );
 
66
  add_filter( 'limit_login_whitelist_ip', array( $this, 'check_whitelist_ips' ), 10, 2 );
67
  add_filter( 'limit_login_whitelist_usernames', array( $this, 'check_whitelist_usernames' ), 10, 2 );
68
  add_filter( 'limit_login_blacklist_ip', array( $this, 'check_blacklist_ips' ), 10, 2 );
@@ -133,7 +134,132 @@ class Limit_Login_Attempts
133
 
134
  add_action('wp_ajax_limit-login-unlock', array( $this, 'ajax_unlock' ) );
135
  }
136
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  public function check_xmlrpc_lock()
138
  {
139
  if ( is_user_logged_in() || $this->is_ip_whitelisted() )
@@ -159,7 +285,7 @@ class Limit_Login_Attempts
159
  }
160
 
161
  public function check_blacklist_usernames( $allow, $username ) {
162
- return in_array( $username, (array) $this->get_option( 'blacklist_usernames' ) );
163
  }
164
 
165
  public function ip_in_range( $ip, $list )
@@ -884,6 +1010,8 @@ class Limit_Login_Attempts
884
  $msg .= sprintf( _n( 'Please try again in %d minute.', 'Please try again in %d minutes.', $when, 'limit-login-attempts-reloaded' ), $when );
885
  }
886
 
 
 
887
  return $msg;
888
  }
889
 
@@ -1040,25 +1168,14 @@ class Limit_Login_Attempts
1040
  }
1041
 
1042
  /**
1043
- * Get correct remote address
1044
- *
1045
- * @param string $type_name
1046
- *
1047
- * @return string
1048
- */
1049
  public function get_address() {
1050
 
1051
- if ( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) && filter_var( $_SERVER['HTTP_X_FORWARDED_FOR'], FILTER_VALIDATE_IP ) )
1052
- $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
1053
-
1054
- elseif ( !empty( $_SERVER['HTTP_X_SUCURI_CLIENTIP'] ) && filter_var( $_SERVER['HTTP_X_SUCURI_CLIENTIP'], FILTER_VALIDATE_IP ) )
1055
- $ip = $_SERVER['HTTP_X_SUCURI_CLIENTIP'];
1056
-
1057
- elseif ( isset( $_SERVER['REMOTE_ADDR'] ) )
1058
- $ip = $_SERVER['REMOTE_ADDR'];
1059
-
1060
- else
1061
- $ip = '';
1062
 
1063
  $ip = preg_replace('/^(\d+\.\d+\.\d+\.\d+):\d+$/', '\1', $ip);
1064
 
@@ -1076,16 +1193,27 @@ class Limit_Login_Attempts
1076
  $now = time();
1077
  $lockouts = ! is_null( $lockouts ) ? $lockouts : $this->get_option( 'lockouts' );
1078
 
 
 
1079
  /* remove old lockouts */
1080
  if ( is_array( $lockouts ) ) {
1081
  foreach ( $lockouts as $ip => $lockout ) {
1082
  if ( $lockout < $now ) {
1083
  unset( $lockouts[ $ip ] );
 
 
 
 
 
 
 
1084
  }
1085
  }
1086
  $this->update_option( 'lockouts', $lockouts );
1087
  }
1088
 
 
 
1089
  /* remove retries that are no longer valid */
1090
  $valid = ! is_null( $valid ) ? $valid : $this->get_option( 'retries_valid' );
1091
  $retries = ! is_null( $retries ) ? $retries : $this->get_option( 'retries' );
@@ -1163,9 +1291,9 @@ class Limit_Login_Attempts
1163
  {
1164
  $this->update_option('allowed_retries', (int)$_POST['allowed_retries'] );
1165
  $this->update_option('lockout_duration', (int)$_POST['lockout_duration'] * 60 );
1166
- $this->update_option('valid_duration', (int)$_POST['valid_duration'] * 3600 );
1167
- $this->update_option('allowed_lockouts', (int)$_POST['allowed_lockouts'] );
1168
- $this->update_option('long_duration', (int)$_POST['long_duration'] * 3600 );
1169
  $this->update_option('notify_email_after', (int)$_POST['email_after'] );
1170
 
1171
  $this->update_option('admin_notify_email', sanitize_email( $_POST['admin_notify_email'] ) );
63
  public function hooks_init() {
64
  add_action( 'plugins_loaded', array( $this, 'setup' ), 9999 );
65
  add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );
66
+ add_action( 'after_password_reset', array( $this, 'after_password_reset' ) );
67
  add_filter( 'limit_login_whitelist_ip', array( $this, 'check_whitelist_ips' ), 10, 2 );
68
  add_filter( 'limit_login_whitelist_usernames', array( $this, 'check_whitelist_usernames' ), 10, 2 );
69
  add_filter( 'limit_login_blacklist_ip', array( $this, 'check_blacklist_ips' ), 10, 2 );
134
 
135
  add_action('wp_ajax_limit-login-unlock', array( $this, 'ajax_unlock' ) );
136
  }
137
+
138
+ /**
139
+ * @param $user Wp_User
140
+ */
141
+ public function after_password_reset( $user ) {
142
+
143
+ $lockouts = $this->get_option( 'lockouts' );
144
+ $lockouts_log = $this->get_option( 'logged' );
145
+
146
+ if( $user->has_cap( 'administrator' ) ) {
147
+
148
+ if( $this->is_ip_blacklisted() ) {
149
+
150
+ $black_list_ips = $this->get_option( 'blacklist' );
151
+
152
+ if( !empty( $black_list_ips ) ) {
153
+
154
+ foreach ( $black_list_ips as $key => $ip ) {
155
+
156
+ if( $ip === $this->get_address() ) {
157
+
158
+ unset($black_list_ips[$key]);
159
+ }
160
+ }
161
+
162
+ }
163
+
164
+ $this->update_option( 'blacklist', $black_list_ips );
165
+ }
166
+
167
+ if( $this->is_username_blacklisted( $user->data->user_login ) ) {
168
+
169
+ $black_list_usernames = $this->get_option( 'blacklist_usernames' );
170
+
171
+ if( !empty( $black_list_usernames ) ) {
172
+
173
+ foreach ( $black_list_usernames as $key => $login ) {
174
+
175
+ if( $login === $user->data->user_login ) {
176
+
177
+ unset($black_list_usernames[$key]);
178
+ }
179
+ }
180
+
181
+ }
182
+
183
+ $this->update_option( 'blacklist_usernames', $black_list_usernames );
184
+ }
185
+
186
+ $admin_ip = $this->get_address();
187
+ $admin_ip = ($this->get_option('gdpr') ? $this->getHash( $admin_ip ) : $admin_ip );
188
+
189
+ if ( is_array( $lockouts ) && isset( $lockouts[ $admin_ip ] ) ) {
190
+
191
+ unset( $lockouts[ $admin_ip ] );
192
+
193
+ $this->update_option( 'lockouts', $lockouts );
194
+
195
+ if( is_array( $lockouts_log ) && isset( $lockouts_log[ $admin_ip ] ) ) {
196
+
197
+ foreach ( $lockouts_log[ $admin_ip ] as $user_login => &$data ) {
198
+
199
+ $data['unlocked'] = true;
200
+ }
201
+
202
+ $this->update_option( 'logged', $lockouts_log );
203
+ }
204
+ }
205
+
206
+ $valid = $this->get_option( 'retries_valid' );
207
+
208
+ if ( is_array( $valid ) && isset( $valid[ $admin_ip ] ) ) {
209
+
210
+ unset( $valid[ $admin_ip ] );
211
+
212
+ $this->update_option( 'retries_valid', $valid );
213
+ }
214
+
215
+ $retries = $this->get_option( 'retries' );
216
+
217
+ if ( is_array( $retries ) && isset( $retries[ $admin_ip ] ) ) {
218
+
219
+ unset( $retries[ $admin_ip ] );
220
+
221
+ $this->update_option( 'retries', $retries );
222
+ }
223
+
224
+ } else {
225
+
226
+ $user_ip = $this->get_address();
227
+ $user_ip = ($this->get_option('gdpr') ? $this->getHash( $user_ip ) : $user_ip );
228
+
229
+ if ( isset( $lockouts_log[ $user_ip ] ) && is_array( $lockouts_log[ $user_ip ] ) ) {
230
+
231
+ $last_unlocked_time = 0;
232
+ foreach ( $lockouts_log[ $user_ip ] as $user_login => $data ) {
233
+
234
+ if( !isset( $data['unlocked'] ) || !$data['unlocked'] ) continue;
235
+
236
+ if( $data['date'] > $last_unlocked_time )
237
+ $last_unlocked_time = $data['date'];
238
+ }
239
+
240
+ if ( is_array( $lockouts ) && isset( $lockouts[ $user_ip ] ) &&
241
+ ( $last_unlocked_time === 0 ||
242
+ ( ( time() - $last_unlocked_time ) ) > ( $this->get_option( 'lockout_duration' ) ) ) ) {
243
+
244
+ unset( $lockouts[ $user_ip ] );
245
+
246
+ if( is_array( $lockouts_log ) && isset( $lockouts_log[ $user_ip ] ) ) {
247
+
248
+ foreach ( $lockouts_log[ $user_ip ] as $user_login => &$data ) {
249
+
250
+ $data['unlocked'] = true;
251
+ }
252
+
253
+ $this->update_option( 'logged', $lockouts_log );
254
+ }
255
+
256
+ $this->update_option( 'lockouts', $lockouts );
257
+ }
258
+
259
+ }
260
+ }
261
+ }
262
+
263
  public function check_xmlrpc_lock()
264
  {
265
  if ( is_user_logged_in() || $this->is_ip_whitelisted() )
285
  }
286
 
287
  public function check_blacklist_usernames( $allow, $username ) {
288
+ return in_array( $username, (array) $this->get_option( 'blacklist_usernames' ) );
289
  }
290
 
291
  public function ip_in_range( $ip, $list )
1010
  $msg .= sprintf( _n( 'Please try again in %d minute.', 'Please try again in %d minutes.', $when, 'limit-login-attempts-reloaded' ), $when );
1011
  }
1012
 
1013
+ $msg .= '<br><br>'. sprintf( __( 'You can also try <a href="%s">resetting your password</a> and that should help you to log in.', 'limit-login-attempts-reloaded' ), wp_lostpassword_url() );
1014
+
1015
  return $msg;
1016
  }
1017
 
1168
  }
1169
 
1170
  /**
1171
+ * Get correct remote address
1172
+ *
1173
+ * @return string
1174
+ *
1175
+ */
 
1176
  public function get_address() {
1177
 
1178
+ $ip = ( isset( $_SERVER['REMOTE_ADDR'] ) && !empty( $_SERVER['REMOTE_ADDR'] ) ) ? $_SERVER['REMOTE_ADDR'] : '';
 
 
 
 
 
 
 
 
 
 
1179
 
1180
  $ip = preg_replace('/^(\d+\.\d+\.\d+\.\d+):\d+$/', '\1', $ip);
1181
 
1193
  $now = time();
1194
  $lockouts = ! is_null( $lockouts ) ? $lockouts : $this->get_option( 'lockouts' );
1195
 
1196
+ $log = $this->get_option( 'logged' );
1197
+
1198
  /* remove old lockouts */
1199
  if ( is_array( $lockouts ) ) {
1200
  foreach ( $lockouts as $ip => $lockout ) {
1201
  if ( $lockout < $now ) {
1202
  unset( $lockouts[ $ip ] );
1203
+
1204
+ if( is_array( $log ) && isset( $log[ $ip ] ) ) {
1205
+ foreach ( $log[ $ip ] as $user_login => &$data ) {
1206
+
1207
+ $data['unlocked'] = true;
1208
+ }
1209
+ }
1210
  }
1211
  }
1212
  $this->update_option( 'lockouts', $lockouts );
1213
  }
1214
 
1215
+ $this->update_option( 'logged', $log );
1216
+
1217
  /* remove retries that are no longer valid */
1218
  $valid = ! is_null( $valid ) ? $valid : $this->get_option( 'retries_valid' );
1219
  $retries = ! is_null( $retries ) ? $retries : $this->get_option( 'retries' );
1291
  {
1292
  $this->update_option('allowed_retries', (int)$_POST['allowed_retries'] );
1293
  $this->update_option('lockout_duration', (int)$_POST['lockout_duration'] * 60 );
1294
+ // $this->update_option('valid_duration', (int)$_POST['valid_duration'] * 3600 );
1295
+ // $this->update_option('allowed_lockouts', (int)$_POST['allowed_lockouts'] );
1296
+ // $this->update_option('long_duration', (int)$_POST['long_duration'] * 3600 );
1297
  $this->update_option('notify_email_after', (int)$_POST['email_after'] );
1298
 
1299
  $this->update_option('admin_notify_email', sanitize_email( $_POST['admin_notify_email'] ) );
limit-login-attempts-reloaded.php CHANGED
@@ -5,11 +5,9 @@ Description: Limit the rate of login attempts, including by way of cookies and f
5
  Author: WPChef
6
  Author URI: https://wpchef.org
7
  Text Domain: limit-login-attempts-reloaded
8
- Version: 2.7.4
9
 
10
  Copyright 2008 - 2012 Johan Eenfeldt, 2016 - 2019 WPChef
11
-
12
- Thanks to Michael Skerwiderski for reverse proxy handling suggestions.
13
  */
14
 
15
  /***************************************************************************************
5
  Author: WPChef
6
  Author URI: https://wpchef.org
7
  Text Domain: limit-login-attempts-reloaded
8
+ Version: 2.8.0
9
 
10
  Copyright 2008 - 2012 Johan Eenfeldt, 2016 - 2019 WPChef
 
 
11
  */
12
 
13
  /***************************************************************************************
readme.txt CHANGED
@@ -3,7 +3,7 @@ Contributors: wpchefgadget
3
  Tags: brute force, login, security, GDPR, protection
4
  Requires at least: 3.0
5
  Tested up to: 5.1.1
6
- Stable tag: 2.7.4
7
 
8
  Reloaded version of the original Limit Login Attempts plugin for Login Protection by a team of WordPress developers. GDPR compliant.
9
 
@@ -19,7 +19,6 @@ Features:
19
  * Limit the number of attempts to log in using authorization cookies in the same way.
20
  * Informs the user about the remaining retries or lockout time on the login page.
21
  * Optional logging and optional email notification.
22
- * Handles server behind the reverse proxy.
23
  * It is possible to whitelist/blacklist IPs and Usernames.
24
  * Sucuri Website Firewall compatibility.
25
  * **XMLRPC** gateway protection.
@@ -51,6 +50,9 @@ Based on the original code from Limit Login Attemps plugin by Johan Eenfeldt.
51
 
52
  == Changelog ==
53
 
 
 
 
54
  = 2.7.4 =
55
  * The lockout alerts can be sent to a configurable email address now.
56
 
3
  Tags: brute force, login, security, GDPR, protection
4
  Requires at least: 3.0
5
  Tested up to: 5.1.1
6
+ Stable tag: 2.8.0
7
 
8
  Reloaded version of the original Limit Login Attempts plugin for Login Protection by a team of WordPress developers. GDPR compliant.
9
 
19
  * Limit the number of attempts to log in using authorization cookies in the same way.
20
  * Informs the user about the remaining retries or lockout time on the login page.
21
  * Optional logging and optional email notification.
 
22
  * It is possible to whitelist/blacklist IPs and Usernames.
23
  * Sucuri Website Firewall compatibility.
24
  * **XMLRPC** gateway protection.
50
 
51
  == Changelog ==
52
 
53
+ = 2.8.0 =
54
+ * The plugin doesn't trust any IP addresses other than _SERVER["REMOTE_ADDR"] anymore. Trusting other IP origins make protection useless b/c they can be easily faked. This new version provides a way of secure IP unlocking for those sites that use a reverse proxy coupled with misconfigurated servers that populate _SERVER["REMOTE_ADDR"] with wrong IPs which leads to mass blocking of users.
55
+
56
  = 2.7.4 =
57
  * The lockout alerts can be sent to a configurable email address now.
58
 
views/options-page.php CHANGED
@@ -111,6 +111,7 @@ $admin_email_placeholder = (!is_multisite()) ? get_option( 'admin_email' ) : get
111
  value="<?php echo( $this->get_option( 'lockout_duration' ) / 60 ); ?>"
112
  name="lockout_duration"/> <?php echo __( 'minutes lockout', 'limit-login-attempts-reloaded' ); ?>
113
  <br/>
 
114
  <input type="text" size="3" maxlength="4"
115
  value="<?php echo( $this->get_option( 'allowed_lockouts' ) ); ?>"
116
  name="allowed_lockouts"/> <?php echo __( 'lockouts increase lockout time to', 'limit-login-attempts-reloaded' ); ?>
@@ -120,6 +121,7 @@ $admin_email_placeholder = (!is_multisite()) ? get_option( 'admin_email' ) : get
120
  <input type="text" size="3" maxlength="4"
121
  value="<?php echo( $this->get_option( 'valid_duration' ) / 3600 ); ?>"
122
  name="valid_duration"/> <?php echo __( 'hours until retries are reset', 'limit-login-attempts-reloaded' ); ?>
 
123
  </td>
124
  </tr>
125
  <tr>
111
  value="<?php echo( $this->get_option( 'lockout_duration' ) / 60 ); ?>"
112
  name="lockout_duration"/> <?php echo __( 'minutes lockout', 'limit-login-attempts-reloaded' ); ?>
113
  <br/>
114
+ <?php /*
115
  <input type="text" size="3" maxlength="4"
116
  value="<?php echo( $this->get_option( 'allowed_lockouts' ) ); ?>"
117
  name="allowed_lockouts"/> <?php echo __( 'lockouts increase lockout time to', 'limit-login-attempts-reloaded' ); ?>
121
  <input type="text" size="3" maxlength="4"
122
  value="<?php echo( $this->get_option( 'valid_duration' ) / 3600 ); ?>"
123
  name="valid_duration"/> <?php echo __( 'hours until retries are reset', 'limit-login-attempts-reloaded' ); ?>
124
+ */ ?>
125
  </td>
126
  </tr>
127
  <tr>