Login Security Solution - Version 0.18.0

Version Description

  • Keep legit user from having to repeatedly reset pw during active attacks against their user name.
Download this release

Release Info

Developer convissor
Plugin Icon wp plugin Login Security Solution
Version 0.18.0
Comparing to
See all releases

Code changes from version 0.17.0 to 0.18.0

languages/login-security-solution.pot CHANGED
@@ -2,9 +2,9 @@
2
  # This file is distributed under the same license as the Login Security Solution package.
3
  msgid ""
4
  msgstr ""
5
- "Project-Id-Version: Login Security Solution 0.11.0\n"
6
  "Report-Msgid-Bugs-To: http://wordpress.org/tag/login-security-solution\n"
7
- "POT-Creation-Date: 2012-06-30 01:24:19+00:00\n"
8
  "MIME-Version: 1.0\n"
9
  "Content-Type: text/plain; charset=UTF-8\n"
10
  "Content-Transfer-Encoding: 8bit\n"
@@ -12,171 +12,205 @@ msgstr ""
12
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13
  "Language-Team: LANGUAGE <LL@li.org>\n"
14
 
15
- #: login-security-solution.php:470
16
  msgid "Invalid username or password."
17
  msgstr ""
18
 
19
- #: login-security-solution.php:476 tests/LoginErrorsTest.php:117
20
  #: tests/LoginErrorsTest.php:129
21
  msgid "Password reset is not allowed for this user"
22
  msgstr ""
23
 
24
- #: login-security-solution.php:501 tests/LoginMessageTest.php:66
25
  msgid "It has been over %d minutes since your last action."
26
  msgstr ""
27
 
28
- #: login-security-solution.php:502 tests/LoginMessageTest.php:67
29
  msgid "Please log back in."
30
  msgstr ""
31
 
32
- #: login-security-solution.php:505 tests/LoginMessageTest.php:77
33
  msgid "The grace period for changing your password has expired."
34
  msgstr ""
35
 
36
- #: login-security-solution.php:506 tests/LoginMessageTest.php:78
37
  msgid "Please submit this form to reset your password."
38
  msgstr ""
39
 
40
- #: login-security-solution.php:509 tests/LoginMessageTest.php:88
41
  msgid "Your password must be reset."
42
  msgstr ""
43
 
44
- #: login-security-solution.php:510 tests/LoginMessageTest.php:89
45
  msgid "Please submit this form to reset it."
46
  msgstr ""
47
 
48
- #: login-security-solution.php:513 tests/LoginMessageTest.php:104
49
  msgid "Your password has expired. Please log and change it."
50
  msgstr ""
51
 
52
- #: login-security-solution.php:514 tests/LoginMessageTest.php:105
53
  msgid "We provide a %d minute grace period to do so."
54
  msgstr ""
55
 
56
- #: login-security-solution.php:517 tests/LoginMessageTest.php:115
57
  msgid "The password you tried to create is not secure. Please try again."
58
  msgstr ""
59
 
60
- #: login-security-solution.php:523 tests/LoginMessageTest.php:129
61
  #: tests/LoginMessageTest.php:144
62
  msgid "The site is undergoing maintenance."
63
  msgstr ""
64
 
65
- #: login-security-solution.php:524 tests/LoginMessageTest.php:130
66
  #: tests/LoginMessageTest.php:145
67
  msgid "Please try again later."
68
  msgstr ""
69
 
70
- #: login-security-solution.php:593
71
  msgid "Passwords can not be reused."
72
  msgstr ""
73
 
74
- #: login-security-solution.php:740
75
  msgid "ERROR"
76
  msgstr ""
77
 
78
- #: login-security-solution.php:860
79
  msgid "Component Count Value from Current Attempt"
80
  msgstr ""
81
 
82
- #: login-security-solution.php:862
83
  msgid "Network IP %5d %s"
84
  msgstr ""
85
 
86
- #: login-security-solution.php:864
87
  msgid "Username %5d %s"
88
  msgstr ""
89
 
90
- #: login-security-solution.php:866
91
  msgid "Password MD5 %5d %s"
92
  msgstr ""
93
 
94
- #: login-security-solution.php:1647
 
 
 
 
95
  msgid "Your website, %s, may have been broken in to."
96
  msgstr ""
97
 
98
- #: login-security-solution.php:1650
99
  msgid ""
100
  "Someone just logged in using the following components. Prior to that, some "
101
  "combination of those components were a part of %d failed attempts to log in "
102
  "during the past %d minutes:"
103
  msgstr ""
104
 
105
- #: login-security-solution.php:1655
 
 
 
 
 
 
 
 
106
  msgid ""
107
  "The user has been logged out and will be required to confirm their identity "
108
  "via the password reset functionality."
109
  msgstr ""
110
 
111
- #: login-security-solution.php:1684
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  msgid "Your website, %s, is undergoing a brute force attack."
113
  msgstr ""
114
 
115
- #: login-security-solution.php:1687
116
  msgid ""
117
  "There have been at least %d failed attempts to log in during the past %d "
118
  "minutes that used one or more of the following components:"
119
  msgstr ""
120
 
121
- #: login-security-solution.php:1692
122
  msgid ""
123
  "The %s plugin for WordPress is repelling the attack by making their login "
124
  "failures take a very long time."
125
  msgstr ""
126
 
127
- #: login-security-solution.php:2014
128
  msgid "Password not set."
129
  msgstr ""
130
 
131
- #: login-security-solution.php:2029
132
  msgid "Passwords must be strings."
133
  msgstr ""
134
 
135
- #: login-security-solution.php:2047
136
  msgid "Passwords must use ASCII characters."
137
  msgstr ""
138
 
139
- #: login-security-solution.php:2066
140
  msgid "Password is too short."
141
  msgstr ""
142
 
143
- #: login-security-solution.php:2075
144
  msgid "Passwords must either contain numbers or be %d characters long."
145
  msgstr ""
146
 
147
- #: login-security-solution.php:2084
148
  msgid ""
149
  "Passwords must either contain punctuation marks / symbols or be %d "
150
  "characters long."
151
  msgstr ""
152
 
153
- #: login-security-solution.php:2093
154
  msgid ""
155
  "Passwords must either contain upper-case and lower-case letters or be %d "
156
  "characters long."
157
  msgstr ""
158
 
159
- #: login-security-solution.php:2103
160
  msgid "Passwords can't be sequential keys."
161
  msgstr ""
162
 
163
- #: login-security-solution.php:2112
164
  msgid "Passwords can't have that many sequential characters."
165
  msgstr ""
166
 
167
- #: login-security-solution.php:2128
168
  msgid "Passwords can't contain user data."
169
  msgstr ""
170
 
171
- #: login-security-solution.php:2139
172
  msgid "Passwords can't contain site info."
173
  msgstr ""
174
 
175
- #: login-security-solution.php:2148
176
  msgid "Password is too common."
177
  msgstr ""
178
 
179
- #: login-security-solution.php:2157
180
  msgid "Passwords can't be variations of dictionary words."
181
  msgstr ""
182
 
2
  # This file is distributed under the same license as the Login Security Solution package.
3
  msgid ""
4
  msgstr ""
5
+ "Project-Id-Version: Login Security Solution 0.18.0\n"
6
  "Report-Msgid-Bugs-To: http://wordpress.org/tag/login-security-solution\n"
7
+ "POT-Creation-Date: 2012-07-11 16:41:18+00:00\n"
8
  "MIME-Version: 1.0\n"
9
  "Content-Type: text/plain; charset=UTF-8\n"
10
  "Content-Transfer-Encoding: 8bit\n"
12
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13
  "Language-Team: LANGUAGE <LL@li.org>\n"
14
 
15
+ #: login-security-solution.php:495
16
  msgid "Invalid username or password."
17
  msgstr ""
18
 
19
+ #: login-security-solution.php:501 tests/LoginErrorsTest.php:117
20
  #: tests/LoginErrorsTest.php:129
21
  msgid "Password reset is not allowed for this user"
22
  msgstr ""
23
 
24
+ #: login-security-solution.php:526 tests/LoginMessageTest.php:66
25
  msgid "It has been over %d minutes since your last action."
26
  msgstr ""
27
 
28
+ #: login-security-solution.php:527 tests/LoginMessageTest.php:67
29
  msgid "Please log back in."
30
  msgstr ""
31
 
32
+ #: login-security-solution.php:530 tests/LoginMessageTest.php:77
33
  msgid "The grace period for changing your password has expired."
34
  msgstr ""
35
 
36
+ #: login-security-solution.php:531 tests/LoginMessageTest.php:78
37
  msgid "Please submit this form to reset your password."
38
  msgstr ""
39
 
40
+ #: login-security-solution.php:534 tests/LoginMessageTest.php:88
41
  msgid "Your password must be reset."
42
  msgstr ""
43
 
44
+ #: login-security-solution.php:535 tests/LoginMessageTest.php:89
45
  msgid "Please submit this form to reset it."
46
  msgstr ""
47
 
48
+ #: login-security-solution.php:538 tests/LoginMessageTest.php:104
49
  msgid "Your password has expired. Please log and change it."
50
  msgstr ""
51
 
52
+ #: login-security-solution.php:539 tests/LoginMessageTest.php:105
53
  msgid "We provide a %d minute grace period to do so."
54
  msgstr ""
55
 
56
+ #: login-security-solution.php:542 tests/LoginMessageTest.php:115
57
  msgid "The password you tried to create is not secure. Please try again."
58
  msgstr ""
59
 
60
+ #: login-security-solution.php:548 tests/LoginMessageTest.php:129
61
  #: tests/LoginMessageTest.php:144
62
  msgid "The site is undergoing maintenance."
63
  msgstr ""
64
 
65
+ #: login-security-solution.php:549 tests/LoginMessageTest.php:130
66
  #: tests/LoginMessageTest.php:145
67
  msgid "Please try again later."
68
  msgstr ""
69
 
70
+ #: login-security-solution.php:619
71
  msgid "Passwords can not be reused."
72
  msgstr ""
73
 
74
+ #: login-security-solution.php:794
75
  msgid "ERROR"
76
  msgstr ""
77
 
78
+ #: login-security-solution.php:919
79
  msgid "Component Count Value from Current Attempt"
80
  msgstr ""
81
 
82
+ #: login-security-solution.php:921
83
  msgid "Network IP %5d %s"
84
  msgstr ""
85
 
86
+ #: login-security-solution.php:923
87
  msgid "Username %5d %s"
88
  msgstr ""
89
 
90
+ #: login-security-solution.php:925
91
  msgid "Password MD5 %5d %s"
92
  msgstr ""
93
 
94
+ #: login-security-solution.php:1722 login-security-solution.php:1759
95
+ msgid "POTENTIAL INTRUSION AT %s"
96
+ msgstr ""
97
+
98
+ #: login-security-solution.php:1726
99
  msgid "Your website, %s, may have been broken in to."
100
  msgstr ""
101
 
102
+ #: login-security-solution.php:1729
103
  msgid ""
104
  "Someone just logged in using the following components. Prior to that, some "
105
  "combination of those components were a part of %d failed attempts to log in "
106
  "during the past %d minutes:"
107
  msgstr ""
108
 
109
+ #: login-security-solution.php:1735
110
+ msgid ""
111
+ "The user's current IP address is one they have verified with your site in "
112
+ "the past. Therefore, the user will NOT be required to confirm their "
113
+ "identity via the password reset process. An email will be sent to them, "
114
+ "just in case this actually was a breach."
115
+ msgstr ""
116
+
117
+ #: login-security-solution.php:1737
118
  msgid ""
119
  "The user has been logged out and will be required to confirm their identity "
120
  "via the password reset functionality."
121
  msgstr ""
122
 
123
+ #: login-security-solution.php:1763
124
+ msgid ""
125
+ "Someone just logged into your '%s' account at %s. Was it you that logged "
126
+ "in? We are asking because the site is being attacked."
127
+ msgstr ""
128
+
129
+ #: login-security-solution.php:1764
130
+ msgid "IF IT WAS NOT YOU, please do the following right away:"
131
+ msgstr ""
132
+
133
+ #: login-security-solution.php:1765
134
+ msgid "1) Log into %s and change your password."
135
+ msgstr ""
136
+
137
+ #: login-security-solution.php:1766
138
+ msgid "2) Send an email to %s letting them know it was not you who logged in."
139
+ msgstr ""
140
+
141
+ #: login-security-solution.php:1792
142
+ msgid "ATTACK HAPPENING TO %s"
143
+ msgstr ""
144
+
145
+ #: login-security-solution.php:1796
146
  msgid "Your website, %s, is undergoing a brute force attack."
147
  msgstr ""
148
 
149
+ #: login-security-solution.php:1799
150
  msgid ""
151
  "There have been at least %d failed attempts to log in during the past %d "
152
  "minutes that used one or more of the following components:"
153
  msgstr ""
154
 
155
+ #: login-security-solution.php:1804
156
  msgid ""
157
  "The %s plugin for WordPress is repelling the attack by making their login "
158
  "failures take a very long time."
159
  msgstr ""
160
 
161
+ #: login-security-solution.php:2155
162
  msgid "Password not set."
163
  msgstr ""
164
 
165
+ #: login-security-solution.php:2170
166
  msgid "Passwords must be strings."
167
  msgstr ""
168
 
169
+ #: login-security-solution.php:2188
170
  msgid "Passwords must use ASCII characters."
171
  msgstr ""
172
 
173
+ #: login-security-solution.php:2207
174
  msgid "Password is too short."
175
  msgstr ""
176
 
177
+ #: login-security-solution.php:2216
178
  msgid "Passwords must either contain numbers or be %d characters long."
179
  msgstr ""
180
 
181
+ #: login-security-solution.php:2225
182
  msgid ""
183
  "Passwords must either contain punctuation marks / symbols or be %d "
184
  "characters long."
185
  msgstr ""
186
 
187
+ #: login-security-solution.php:2234
188
  msgid ""
189
  "Passwords must either contain upper-case and lower-case letters or be %d "
190
  "characters long."
191
  msgstr ""
192
 
193
+ #: login-security-solution.php:2244
194
  msgid "Passwords can't be sequential keys."
195
  msgstr ""
196
 
197
+ #: login-security-solution.php:2253
198
  msgid "Passwords can't have that many sequential characters."
199
  msgstr ""
200
 
201
+ #: login-security-solution.php:2269
202
  msgid "Passwords can't contain user data."
203
  msgstr ""
204
 
205
+ #: login-security-solution.php:2280
206
  msgid "Passwords can't contain site info."
207
  msgstr ""
208
 
209
+ #: login-security-solution.php:2289
210
  msgid "Password is too common."
211
  msgstr ""
212
 
213
+ #: login-security-solution.php:2298
214
  msgid "Passwords can't be variations of dictionary words."
215
  msgstr ""
216
 
login-security-solution.php CHANGED
@@ -6,7 +6,7 @@
6
  * Description: Requires very strong passwords, repels brute force login attacks, prevents login information disclosures, expires idle sessions, notifies admins of attacks and breaches, permits administrators to disable logins for maintenance or emergency reasons and reset all passwords.
7
  *
8
  * Plugin URI: http://wordpress.org/extend/plugins/login-security-solution/
9
- * Version: 0.17.0
10
  * Author: Daniel Convissor
11
  * Author URI: http://www.analysisandsolutions.com/
12
  * License: GPLv2
@@ -164,6 +164,12 @@ class login_security_solution {
164
  */
165
  protected $umk_pw_force_change;
166
 
 
 
 
 
 
 
167
 
168
  /**
169
  * Declares the WordPress action and filter callbacks
@@ -264,6 +270,7 @@ class login_security_solution {
264
  $this->umk_grace_period = self::ID . '-pw-grace-period-start-time';
265
  $this->umk_hashes = self::ID . '-pw-hashes';
266
  $this->umk_last_active = self::ID . '-last-active';
 
267
 
268
  $this->dir_dictionaries = dirname(__FILE__) . '/pw_dictionaries/';
269
  $this->dir_sequences = dirname(__FILE__) . '/pw_sequences/';
@@ -577,6 +584,7 @@ class login_security_solution {
577
  return -1;
578
  }
579
 
 
580
  $this->process_pw_metadata($user->ID, $user_pass);
581
  }
582
 
@@ -619,6 +627,9 @@ class login_security_solution {
619
  // Empty ID means an admin is adding a new user.
620
  if (!empty($user->ID) && !$errors->get_error_codes()) {
621
  $this->process_pw_metadata($user->ID, $user->user_pass);
 
 
 
622
  }
623
 
624
  return $answer;
@@ -634,11 +645,14 @@ class login_security_solution {
634
  * @param WP_User $user the current user
635
  * @return mixed return values provided for unit testing
636
  *
 
 
637
  * @uses login_security_solution::get_network_ip() gets the IP's
638
  * "network" part
639
  * @uses login_security_solution::md5() to hash the password
640
  * @uses login_security_solution::get_login_fail() to see if
641
  * they're over the limit
 
642
  * @uses login_security_solution::$options for the
643
  * login_fail_breach_notify value
644
  * @uses login_security_solution::$options for the
@@ -657,14 +671,31 @@ class login_security_solution {
657
  return -1;
658
  }
659
 
660
- $network_ip = $this->get_network_ip();
 
661
  $pass_md5 = $this->md5(empty($_POST['pwd']) ? '' : $_POST['pwd']);
662
 
663
  $return = 1;
664
  $fails = $this->get_login_fail($network_ip, $user_name, $pass_md5);
665
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
  if ($this->options['login_fail_breach_pw_force_change']
667
- && $fails['total'] >= $this->options['login_fail_breach_pw_force_change'])
 
668
  {
669
  ###$this->log("wp_login(): Breach force change.");
670
  $this->set_pw_force_change($user->ID);
@@ -674,8 +705,13 @@ class login_security_solution {
674
  if ($this->options['login_fail_breach_notify']
675
  && $fails['total'] >= $this->options['login_fail_breach_notify'])
676
  {
 
677
  ###$this->log("wp_login(): Breach notify.");
678
- $this->notify_breach($network_ip, $user_name, $pass_md5, $fails);
 
 
 
 
679
  $return += 4;
680
  }
681
 
@@ -910,6 +946,22 @@ $this->log($sql);
910
  return (bool) get_user_meta($user_ID, $this->umk_pw_force_change, true);
911
  }
912
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
913
  /**
914
  * Obtains the timestamp of when the user's "password grace period"
915
  * started
@@ -1653,20 +1705,21 @@ $this->log($sql);
1653
  * @param string $user_name the user name from the current login form
1654
  * @param string $pass_md5 the md5 hashed new password
1655
  * @param array $fails the data from get_login_fail()
 
1656
  * @return bool
1657
  *
1658
  * @uses login_security_solution::get_notify_counts() for some shared text
1659
  * @uses wp_mail() to send the messages
1660
  */
1661
  protected function notify_breach($network_ip, $user_name, $pass_md5,
1662
- $fails)
1663
  {
1664
  $this->load_plugin_textdomain();
1665
 
1666
  $to = $this->sanitize_whitespace(get_option('admin_email'));
1667
 
1668
  $blog = get_option('blogname');
1669
- $subject = sprintf('POTENTIAL INTRUSION AT %s', $blog);
1670
  $subject = $this->sanitize_whitespace($subject);
1671
 
1672
  $message =
@@ -1676,9 +1729,41 @@ $this->log($sql);
1676
  . sprintf(__("Someone just logged in using the following components. Prior to that, some combination of those components were a part of %d failed attempts to log in during the past %d minutes:", self::ID),
1677
  $fails['total'], $this->options['login_fail_minutes']) . "\n\n"
1678
 
1679
- . $this->get_notify_counts($network_ip, $user_name, $pass_md5, $fails)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1680
 
1681
- . __("The user has been logged out and will be required to confirm their identity via the password reset functionality.", self::ID) . "\n";
 
 
 
 
 
 
 
 
 
 
1682
 
1683
  return wp_mail($to, $subject, $message);
1684
  }
@@ -1704,7 +1789,7 @@ $this->log($sql);
1704
  $to = $this->sanitize_whitespace(get_option('admin_email'));
1705
 
1706
  $blog = get_option('blogname');
1707
- $subject = sprintf('ATTACK HAPPENING TO %s', $blog);
1708
  $subject = $this->sanitize_whitespace($subject);
1709
 
1710
  $message =
@@ -1896,6 +1981,38 @@ $this->log($sql);
1896
  return true;
1897
  }
1898
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1899
  /**
1900
  * Stores the present time in the given user's "last active" metadata
1901
  *
6
  * Description: Requires very strong passwords, repels brute force login attacks, prevents login information disclosures, expires idle sessions, notifies admins of attacks and breaches, permits administrators to disable logins for maintenance or emergency reasons and reset all passwords.
7
  *
8
  * Plugin URI: http://wordpress.org/extend/plugins/login-security-solution/
9
+ * Version: 0.18.0
10
  * Author: Daniel Convissor
11
  * Author URI: http://www.analysisandsolutions.com/
12
  * License: GPLv2
164
  */
165
  protected $umk_pw_force_change;
166
 
167
+ /**
168
+ * Our usermeta key for tracking this user's verified IP addresses
169
+ * @var string
170
+ */
171
+ protected $umk_verified_ips;
172
+
173
 
174
  /**
175
  * Declares the WordPress action and filter callbacks
270
  $this->umk_grace_period = self::ID . '-pw-grace-period-start-time';
271
  $this->umk_hashes = self::ID . '-pw-hashes';
272
  $this->umk_last_active = self::ID . '-last-active';
273
+ $this->umk_verified_ips = self::ID . '-verified-ips';
274
 
275
  $this->dir_dictionaries = dirname(__FILE__) . '/pw_dictionaries/';
276
  $this->dir_sequences = dirname(__FILE__) . '/pw_sequences/';
584
  return -1;
585
  }
586
 
587
+ $this->save_verified_ip($user->ID, $this->get_ip());
588
  $this->process_pw_metadata($user->ID, $user_pass);
589
  }
590
 
627
  // Empty ID means an admin is adding a new user.
628
  if (!empty($user->ID) && !$errors->get_error_codes()) {
629
  $this->process_pw_metadata($user->ID, $user->user_pass);
630
+ if ($user->ID == get_current_user_id()) {
631
+ $this->save_verified_ip($user->ID, $this->get_ip());
632
+ }
633
  }
634
 
635
  return $answer;
645
  * @param WP_User $user the current user
646
  * @return mixed return values provided for unit testing
647
  *
648
+ * @uses login_security_solution::get_ip() to get the
649
+ * $_SERVER['REMOTE_ADDR']
650
  * @uses login_security_solution::get_network_ip() gets the IP's
651
  * "network" part
652
  * @uses login_security_solution::md5() to hash the password
653
  * @uses login_security_solution::get_login_fail() to see if
654
  * they're over the limit
655
+ * @uses login_security_solution::get_verified_ips() to check legitimacy
656
  * @uses login_security_solution::$options for the
657
  * login_fail_breach_notify value
658
  * @uses login_security_solution::$options for the
671
  return -1;
672
  }
673
 
674
+ $ip = $this->get_ip();
675
+ $network_ip = $this->get_network_ip($ip);
676
  $pass_md5 = $this->md5(empty($_POST['pwd']) ? '' : $_POST['pwd']);
677
 
678
  $return = 1;
679
  $fails = $this->get_login_fail($network_ip, $user_name, $pass_md5);
680
 
681
+ /*
682
+ * Keep legitimate users from having to repeatedly reset passwords
683
+ * during active attacks against their user name (password ). Do this
684
+ * if the user's current IP address is not involved with any of the
685
+ * recent failed logins and the current IP address has been verified.
686
+ */
687
+ if (!$fails['network_ip']
688
+ && in_array($ip, $this->get_verified_ips($user->ID)))
689
+ {
690
+ $return += 8;
691
+ $verified_ip = true;
692
+ } else {
693
+ $verified_ip = false;
694
+ }
695
+
696
  if ($this->options['login_fail_breach_pw_force_change']
697
+ && $fails['total'] >= $this->options['login_fail_breach_pw_force_change']
698
+ && !$verified_ip)
699
  {
700
  ###$this->log("wp_login(): Breach force change.");
701
  $this->set_pw_force_change($user->ID);
705
  if ($this->options['login_fail_breach_notify']
706
  && $fails['total'] >= $this->options['login_fail_breach_notify'])
707
  {
708
+ // Send this, even if IP is verified, just in case.
709
  ###$this->log("wp_login(): Breach notify.");
710
+ $this->notify_breach($network_ip, $user_name, $pass_md5, $fails,
711
+ $verified_ip);
712
+ if ($verified_ip) {
713
+ $this->notify_breach_user($user);
714
+ }
715
  $return += 4;
716
  }
717
 
946
  return (bool) get_user_meta($user_ID, $this->umk_pw_force_change, true);
947
  }
948
 
949
+ /**
950
+ * Lists IP addresses known to be good for the user
951
+ *
952
+ * @param int $user_ID the current user's ID number
953
+ * @return array the IP addresses
954
+ */
955
+ protected function get_verified_ips($user_ID) {
956
+ $out = get_user_meta($user_ID, $this->umk_verified_ips, true);
957
+ if (empty($out)) {
958
+ $out = array();
959
+ } elseif (!is_array($out)) {
960
+ $out = (array) $out;
961
+ }
962
+ return $out;
963
+ }
964
+
965
  /**
966
  * Obtains the timestamp of when the user's "password grace period"
967
  * started
1705
  * @param string $user_name the user name from the current login form
1706
  * @param string $pass_md5 the md5 hashed new password
1707
  * @param array $fails the data from get_login_fail()
1708
+ * @param bool $verified_ip is the user coming form a verified ip?
1709
  * @return bool
1710
  *
1711
  * @uses login_security_solution::get_notify_counts() for some shared text
1712
  * @uses wp_mail() to send the messages
1713
  */
1714
  protected function notify_breach($network_ip, $user_name, $pass_md5,
1715
+ $fails, $verified_ip)
1716
  {
1717
  $this->load_plugin_textdomain();
1718
 
1719
  $to = $this->sanitize_whitespace(get_option('admin_email'));
1720
 
1721
  $blog = get_option('blogname');
1722
+ $subject = sprintf(__("POTENTIAL INTRUSION AT %s", self::ID), $blog);
1723
  $subject = $this->sanitize_whitespace($subject);
1724
 
1725
  $message =
1729
  . sprintf(__("Someone just logged in using the following components. Prior to that, some combination of those components were a part of %d failed attempts to log in during the past %d minutes:", self::ID),
1730
  $fails['total'], $this->options['login_fail_minutes']) . "\n\n"
1731
 
1732
+ . $this->get_notify_counts($network_ip, $user_name, $pass_md5, $fails);
1733
+
1734
+ if ($verified_ip) {
1735
+ $message .= __("The user's current IP address is one they have verified with your site in the past. Therefore, the user will NOT be required to confirm their identity via the password reset process. An email will be sent to them, just in case this actually was a breach.", self::ID) . "\n";
1736
+ } else {
1737
+ $message .= __("The user has been logged out and will be required to confirm their identity via the password reset functionality.", self::ID) . "\n";
1738
+ }
1739
+
1740
+ return wp_mail($to, $subject, $message);
1741
+ }
1742
+
1743
+ /**
1744
+ * Sends an email to the current user letting them know a breakin
1745
+ * may have occurred
1746
+ *
1747
+ * @param WP_User $user the current user
1748
+ * @return bool
1749
+ *
1750
+ * @uses wp_mail() to send the messages
1751
+ */
1752
+ protected function notify_breach_user($user)
1753
+ {
1754
+ $this->load_plugin_textdomain();
1755
 
1756
+ $to = $this->sanitize_whitespace($user->user_email);
1757
+
1758
+ $blog = get_option('blogname');
1759
+ $subject = sprintf(__("POTENTIAL INTRUSION AT %s", self::ID), $blog);
1760
+ $subject = $this->sanitize_whitespace($subject);
1761
+
1762
+ $message =
1763
+ sprintf(__("Someone just logged into your '%s' account at %s. Was it you that logged in? We are asking because the site is being attacked.", self::ID), $user->user_login, get_option('siteurl')) . "\n\n"
1764
+ . __("IF IT WAS NOT YOU, please do the following right away:", self::ID) . "\n\n"
1765
+ . sprintf(__("1) Log into %s and change your password.", self::ID), wp_login_url()) . "\n\n"
1766
+ . sprintf(__("2) Send an email to %s letting them know it was not you who logged in.", self::ID), get_option('admin_email')) . "\n";
1767
 
1768
  return wp_mail($to, $subject, $message);
1769
  }
1789
  $to = $this->sanitize_whitespace(get_option('admin_email'));
1790
 
1791
  $blog = get_option('blogname');
1792
+ $subject = sprintf(__("ATTACK HAPPENING TO %s", self::ID), $blog);
1793
  $subject = $this->sanitize_whitespace($subject);
1794
 
1795
  $message =
1981
  return true;
1982
  }
1983
 
1984
+ /**
1985
+ * Stores the user's current IP address
1986
+ *
1987
+ * Note: saves up to 10 adddresses, duplicates are not stored.
1988
+ *
1989
+ * @param int $user_ID the user's id number
1990
+ * @param string $new_ip the ip address to add
1991
+ * @return mixed true on success, 1 if IP is already stored, -1 if IP empty
1992
+ */
1993
+ protected function save_verified_ip($user_ID, $new_ip) {
1994
+ if (!$new_ip) {
1995
+ return -1;
1996
+ }
1997
+
1998
+ $ips = $this->get_verified_ips($user_ID);
1999
+
2000
+ if (in_array($new_ip, $ips)) {
2001
+ return 1;
2002
+ }
2003
+
2004
+ $ips[] = $new_ip;
2005
+
2006
+ $cut = count($ips) - 10;
2007
+ if ($cut > 0) {
2008
+ array_splice($ips, 0, $cut);
2009
+ }
2010
+
2011
+ update_user_meta($user_ID, $this->umk_verified_ips, $ips);
2012
+
2013
+ return true;
2014
+ }
2015
+
2016
  /**
2017
  * Stores the present time in the given user's "last active" metadata
2018
  *
readme.txt CHANGED
@@ -4,7 +4,7 @@ Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=danie
4
  Tags: login, password, passwords, strength, strong, strong passwords, password strength, idle, timeout, maintenance, security, attack, hack, lock, ban, brute force, brute, force, authentication, auth, cookie, users
5
  Requires at least: 3.3
6
  Tested up to: 3.4.1
7
- Stable tag: 0.17.0
8
 
9
  Security against brute force attacks by tracking IP, name, password; requiring very strong passwords. Idle timeout. Maintenance mode. Multisite ready!
10
 
@@ -23,9 +23,11 @@ legitimate users or administrators
23
  This limits attackers ability to effectively probe your site,
24
  so they'll give up and go find an easier target.
25
  + If an account seems breached, the "user" is immediately logged out
26
- and forced to use WordPress' password reset utility. This prevents
27
- any damage from being done and verifies the user's identity. All
28
- without intervention by an administrator.
 
 
29
  + Can notify the administrator of attacks and breaches
30
  + Supports IPv6
31
 
@@ -268,13 +270,17 @@ plugin's login failure process.
268
  Get the translation tools from `http://i18n.svn.wordpress.org/tools/trunk/`
269
  then `cd` into that directory and run:
270
 
271
- php makepot.php wp-plugin -d 'error_reporting=E_ALL^E_STRICT' \
272
  ../login-security-solution \
273
  ../login-security-solution/languages/login-security-solution.pot
274
 
275
 
276
  == Changelog ==
277
 
 
 
 
 
278
  = 0.17.0 =
279
  * Fix network IP query in get_login_fail(). (Bug #1553, deanmarktaylor)
280
  * Rename files holding expected test results. (Bug #1552, deanmarktaylor)
4
  Tags: login, password, passwords, strength, strong, strong passwords, password strength, idle, timeout, maintenance, security, attack, hack, lock, ban, brute force, brute, force, authentication, auth, cookie, users
5
  Requires at least: 3.3
6
  Tested up to: 3.4.1
7
+ Stable tag: 0.18.0
8
 
9
  Security against brute force attacks by tracking IP, name, password; requiring very strong passwords. Idle timeout. Maintenance mode. Multisite ready!
10
 
23
  This limits attackers ability to effectively probe your site,
24
  so they'll give up and go find an easier target.
25
  + If an account seems breached, the "user" is immediately logged out
26
+ and forced to use WordPress' password reset utility. This prevents
27
+ any damage from being done and verifies the user's identity. But
28
+ if the user is coming in from an IP address they have used in the
29
+ past, an email is sent to the user making sure it was them logging in.
30
+ All without intervention by an administrator.
31
  + Can notify the administrator of attacks and breaches
32
  + Supports IPv6
33
 
270
  Get the translation tools from `http://i18n.svn.wordpress.org/tools/trunk/`
271
  then `cd` into that directory and run:
272
 
273
+ php -d 'error_reporting=E_ALL^E_STRICT' makepot.php wp-plugin \
274
  ../login-security-solution \
275
  ../login-security-solution/languages/login-security-solution.pot
276
 
277
 
278
  == Changelog ==
279
 
280
+ = 0.18.0 =
281
+ * Keep legit user from having to repeatedly reset pw during active attacks
282
+ against their user name.
283
+
284
  = 0.17.0 =
285
  * Fix network IP query in get_login_fail(). (Bug #1553, deanmarktaylor)
286
  * Rename files holding expected test results. (Bug #1552, deanmarktaylor)
tests/LoginFailTest.php CHANGED
@@ -195,6 +195,39 @@ class LoginFailTest extends TestCase {
195
  $actual = self::$lss->get_pw_force_change($this->user->ID);
196
  $this->assertTrue($actual, 'get_pw_force_change() return value...');
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  $this->check_mail_file();
199
  }
200
 
@@ -231,6 +264,10 @@ class LoginFailTest extends TestCase {
231
  $options['login_fail_breach_notify'] = 0;
232
  self::$lss->options = $options;
233
 
 
 
 
 
234
  self::$lss->delete_pw_force_change($this->user->ID);
235
 
236
  try {
195
  $actual = self::$lss->get_pw_force_change($this->user->ID);
196
  $this->assertTrue($actual, 'get_pw_force_change() return value...');
197
 
198
+ self::$lss->delete_pw_force_change($this->user->ID);
199
+
200
+ $this->check_mail_file();
201
+ }
202
+
203
+ /**
204
+ * @depends test_wp_login__post_breach_threshold
205
+ */
206
+ public function test_wp_login__post_breach_threshold_verified_ip() {
207
+ global $wpdb;
208
+ self::$mail_file_basename = __METHOD__;
209
+
210
+ $wpdb->query('SAVEPOINT pre_verified_ip');
211
+
212
+ $this->ip = '1.2.33.4';
213
+ $_SERVER['REMOTE_ADDR'] = $this->ip;
214
+ $this->network_ip = '1.2.33';
215
+
216
+ self::$lss->save_verified_ip($this->user->ID, $this->ip);
217
+
218
+ try {
219
+ // Do THE deed.
220
+ $actual = self::$lss->wp_login($this->user_name, $this->user);
221
+ } catch (Exception $e) {
222
+ $this->fail($e->getMessage());
223
+ }
224
+ $this->assertSame(13, $actual, 'Bad return value.');
225
+
226
+ $actual = self::$lss->get_pw_force_change($this->user->ID);
227
+ $this->assertFalse($actual, 'get_pw_force_change() return value...');
228
+
229
+ $wpdb->query('ROLLBACK TO pre_verified_ip');
230
+
231
  $this->check_mail_file();
232
  }
233
 
264
  $options['login_fail_breach_notify'] = 0;
265
  self::$lss->options = $options;
266
 
267
+ $this->ip = '1.2.38.4';
268
+ $_SERVER['REMOTE_ADDR'] = $this->ip;
269
+ $this->network_ip = '1.2.38';
270
+
271
  self::$lss->delete_pw_force_change($this->user->ID);
272
 
273
  try {
tests/TestCase.php CHANGED
@@ -360,7 +360,7 @@ abstract class TestCase extends PHPUnit_Framework_TestCase {
360
  $contents = 'To: ' . implode(', ', (array) $to) . "\n"
361
  . "Subject: $subject\n\n$message";
362
 
363
- return file_put_contents(self::$mail_file, $contents);
364
  }
365
 
366
  /**
360
  $contents = 'To: ' . implode(', ', (array) $to) . "\n"
361
  . "Subject: $subject\n\n$message";
362
 
363
+ return file_put_contents(self::$mail_file, $contents, FILE_APPEND);
364
  }
365
 
366
  /**
tests/VerifiedIpTest.php ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Test the verified IP saving and retrieving
5
+ *
6
+ * @package login-security-solution
7
+ * @author Daniel Convissor <danielc@analysisandsolutions.com>
8
+ * @copyright The Analysis and Solutions Company, 2012
9
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
10
+ */
11
+
12
+ /**
13
+ * Get the class we will use for testing
14
+ */
15
+ require_once dirname(__FILE__) . '/TestCase.php';
16
+
17
+
18
+ /**
19
+ * Test the verified IP saving and retrieving
20
+ *
21
+ * @package login-security-solution
22
+ * @author Daniel Convissor <danielc@analysisandsolutions.com>
23
+ * @copyright The Analysis and Solutions Company, 2012
24
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
25
+ */
26
+ class VerifiedIpTest extends TestCase {
27
+ protected static $ip_1 = '1.2.3.4';
28
+
29
+
30
+ public static function setUpBeforeClass() {
31
+ parent::$db_needed = true;
32
+ parent::set_up_before_class();
33
+ }
34
+
35
+ public function setUp() {
36
+ parent::setUp();
37
+ }
38
+
39
+
40
+ public function test_get_verified_ips__empty() {
41
+ global $wpdb;
42
+
43
+ $wpdb->query('SAVEPOINT empty');
44
+
45
+ $actual = self::$lss->get_verified_ips($this->user->ID);
46
+ $this->assertSame(array(), $actual);
47
+ }
48
+
49
+ public function test_save_verified_ip__non_array_edge_case() {
50
+ update_user_meta($this->user->ID, self::$lss->umk_verified_ips, 'foo');
51
+
52
+ $actual = self::$lss->get_verified_ips($this->user->ID);
53
+ $this->assertEquals(array('foo'), $actual);
54
+
55
+ delete_user_meta($this->user->ID, self::$lss->umk_verified_ips);
56
+ }
57
+
58
+ public function test_save_verified_ip__new() {
59
+ $actual = self::$lss->save_verified_ip($this->user->ID, self::$ip_1);
60
+ $this->assertTrue($actual);
61
+ }
62
+
63
+ /**
64
+ * @depends test_save_verified_ip__new
65
+ */
66
+ public function test_save_verified_ip__exists() {
67
+ $actual = self::$lss->save_verified_ip($this->user->ID, self::$ip_1);
68
+ $this->assertSame(1, $actual);
69
+ }
70
+
71
+ /**
72
+ * @depends test_save_verified_ip__exists
73
+ */
74
+ public function test_get_verified_ips__one() {
75
+ global $wpdb;
76
+
77
+ $actual = self::$lss->get_verified_ips($this->user->ID);
78
+ $this->assertEquals(array(self::$ip_1), $actual);
79
+
80
+ $wpdb->query('ROLLBACK TO empty');
81
+ wp_cache_reset();
82
+
83
+ $actual = self::$lss->get_verified_ips($this->user->ID);
84
+ $this->assertSame(array(), $actual);
85
+ }
86
+
87
+ /**
88
+ * @depends test_get_verified_ips__one
89
+ */
90
+ public function test_save_verified_ip__overflow() {
91
+ global $wpdb;
92
+
93
+ self::$lss->save_verified_ip($this->user->ID, 'a');
94
+ self::$lss->save_verified_ip($this->user->ID, 'b');
95
+ self::$lss->save_verified_ip($this->user->ID, 'c');
96
+ self::$lss->save_verified_ip($this->user->ID, 'd');
97
+ self::$lss->save_verified_ip($this->user->ID, 'e');
98
+ self::$lss->save_verified_ip($this->user->ID, 'f');
99
+ self::$lss->save_verified_ip($this->user->ID, 'g');
100
+ self::$lss->save_verified_ip($this->user->ID, 'h');
101
+ self::$lss->save_verified_ip($this->user->ID, 'i');
102
+ self::$lss->save_verified_ip($this->user->ID, 'j');
103
+ self::$lss->save_verified_ip($this->user->ID, 'k');
104
+
105
+ $expected = range('b', 'k');
106
+ $actual = self::$lss->get_verified_ips($this->user->ID);
107
+ $this->assertEquals($expected, $actual);
108
+
109
+ $wpdb->query('ROLLBACK TO empty');
110
+ wp_cache_reset();
111
+ }
112
+
113
+ /**
114
+ * @depends test_save_verified_ip__overflow
115
+ */
116
+ public function test_password_reset__normal() {
117
+ global $wpdb;
118
+
119
+ $ip = '3.4.5.6';
120
+ $_SERVER['REMOTE_ADDR'] = $ip;
121
+
122
+ $actual = self::$lss->password_reset($this->user, 'some 1 Needs!');
123
+ $this->assertNull($actual, 'password_reset() should return null.');
124
+
125
+ // Check the outcome.
126
+ $actual = self::$lss->get_verified_ips($this->user->ID);
127
+ $this->assertSame(array($ip), $actual, 'Expected IP missing.');
128
+
129
+ $wpdb->query('ROLLBACK TO empty');
130
+ wp_cache_reset();
131
+ }
132
+
133
+ /**
134
+ * @depends test_password_reset__normal
135
+ */
136
+ public function test_profile_update__normal() {
137
+ global $current_user;
138
+
139
+ $ip = '4.5.6.7';
140
+ $_SERVER['REMOTE_ADDR'] = $ip;
141
+ // So user id = current user id in our profile update errors method.
142
+ $current_user = $this->user;
143
+
144
+ $errors = new WP_Error;
145
+ $actual = self::$lss->user_profile_update_errors($errors, 1, $this->user);
146
+ $this->assertTrue($actual, 'Bad return value.');
147
+
148
+ // Check the outcome.
149
+ $actual = self::$lss->get_verified_ips($this->user->ID);
150
+ $this->assertSame(array($ip), $actual, 'Expected IP missing.');
151
+ }
152
+ }
tests/expected/LoginFailTest--test_wp_login__post_breach_threshold_verified_ip ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ To: %a
2
+ Subject: POTENTIAL INTRUSION AT %a
3
+
4
+ Your website, %s, may have been broken in to.
5
+
6
+ Someone just logged in using the following components. Prior to that, some combination of those components were a part of 4 failed attempts to log in during the past 60 minutes:
7
+
8
+ Component Count Value from Current Attempt
9
+ ------------ ----- --------------------------------
10
+ Network IP 0 1.2.33
11
+ Username 4 test
12
+ Password MD5 %d %s
13
+
14
+ The user's current IP address is one they have verified with your site in the past. Therefore, the user will NOT be required to confirm their identity via the password reset process. An email will be sent to them, just in case this actually was a breach.
15
+ To: %a
16
+ Subject: POTENTIAL INTRUSION AT %a
17
+
18
+ Someone just logged into your '%s' account at %s. Was it you that logged in? We are asking because the site is being attacked.
19
+
20
+ IF IT WAS NOT YOU, please do the following right away:
21
+
22
+ 1) Log into %s and change your password.
23
+
24
+ 2) Send an email to %s letting them know it was not you who logged in.