Version Description
- Initial import to
plugins.svn.wordpress.org
.
Download this release
Release Info
Developer | convissor |
Plugin | Login Security Solution |
Version | 0.0.4 |
Comparing to | |
See all releases |
Version 0.0.4
- admin.inc +830 -0
- languages/login-security-solution.pot +203 -0
- login-security-solution.php +2001 -0
- pw_dictionaries/test.txt +4 -0
- pw_sequences/us-101-keyboard.txt +1 -0
- readme.txt +259 -0
- tests/Accessor.php +60 -0
- tests/DisableLoginTest.php +73 -0
- tests/IdleTest.php +226 -0
- tests/IpTest.php +161 -0
- tests/LoginErrorsTest.php +133 -0
- tests/LoginFailTest.php +250 -0
- tests/LoginMessageTest.php +131 -0
- tests/PasswordChangeTest.php +291 -0
- tests/PasswordExpirationTest.php +281 -0
- tests/PasswordForceChangeTest.php +90 -0
- tests/PasswordValidationTest.php +660 -0
- tests/TestCase.php +345 -0
- tests/expected/LoginFailTest::test_process_login_fail__post_threshold +14 -0
- tests/expected/LoginFailTest::test_wp_login__post_breach_threshold +14 -0
- tests/password-validation-profiler.php +39 -0
- utilities/reduce-dictionary-files.php +65 -0
admin.inc
ADDED
@@ -0,0 +1,830 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* The user interface and activation/deactivation methods for administering
|
5 |
+
* the Login Security Solution WordPress plugin
|
6 |
+
*
|
7 |
+
* @package login-security-solution
|
8 |
+
* @link http://wordpress.org/extend/plugins/login-security-solution/
|
9 |
+
* @version 0.0.1
|
10 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
11 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
12 |
+
* @copyright The Analysis and Solutions Company, 2012
|
13 |
+
*/
|
14 |
+
|
15 |
+
/**
|
16 |
+
* The user interface and activation/deactivation methods for administering
|
17 |
+
* the Login Security Solution WordPress plugin
|
18 |
+
*
|
19 |
+
* @package login-security-solution
|
20 |
+
* @link http://wordpress.org/extend/plugins/login-security-solution/
|
21 |
+
* @version 0.0.1
|
22 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
23 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
24 |
+
* @copyright The Analysis and Solutions Company, 2012
|
25 |
+
*/
|
26 |
+
class login_security_solution_admin extends login_security_solution {
|
27 |
+
/**
|
28 |
+
* Metadata and labels for each element of the plugin's options
|
29 |
+
* @var array
|
30 |
+
*/
|
31 |
+
protected $fields;
|
32 |
+
|
33 |
+
/**
|
34 |
+
* Key for the change password "don't remind me" checkbox
|
35 |
+
* @var string
|
36 |
+
*/
|
37 |
+
protected $key_checkbox_remind = 'check_remind';
|
38 |
+
|
39 |
+
/**
|
40 |
+
* Key for the change password "confirmation" checkbox
|
41 |
+
* @var string
|
42 |
+
*/
|
43 |
+
protected $key_checkbox_require = 'check_reqire';
|
44 |
+
|
45 |
+
/**
|
46 |
+
* ID slug for the plugin's password change page
|
47 |
+
* @var string
|
48 |
+
*/
|
49 |
+
protected $option_pw_force_change_name;
|
50 |
+
|
51 |
+
/**
|
52 |
+
* Text for the plugin's password change page "don't remind me" button
|
53 |
+
* @var string
|
54 |
+
*/
|
55 |
+
protected $text_button_remind;
|
56 |
+
|
57 |
+
/**
|
58 |
+
* Text for the plugin's password change page require button
|
59 |
+
* @var string
|
60 |
+
*/
|
61 |
+
protected $text_button_require;
|
62 |
+
|
63 |
+
/**
|
64 |
+
* Title for the plugin's settings page
|
65 |
+
* @var string
|
66 |
+
*/
|
67 |
+
protected $text_settings;
|
68 |
+
|
69 |
+
/**
|
70 |
+
* Title for the plugin's password change page
|
71 |
+
* @var string
|
72 |
+
*/
|
73 |
+
protected $text_pw_force_change;
|
74 |
+
|
75 |
+
|
76 |
+
/**
|
77 |
+
* Sets the object's properties and options
|
78 |
+
*
|
79 |
+
* @return void
|
80 |
+
*
|
81 |
+
* @uses login_security_solution::initialize() to set the object's
|
82 |
+
* properties
|
83 |
+
* @uses login_security_solution_admin::set_fields() to populate the
|
84 |
+
* $fields property
|
85 |
+
*/
|
86 |
+
public function __construct() {
|
87 |
+
$this->initialize();
|
88 |
+
$this->set_fields();
|
89 |
+
|
90 |
+
// Combine plugin's name with translation already in WP.
|
91 |
+
$this->text_settings = self::NAME . ' ' . __('Settings');
|
92 |
+
|
93 |
+
// NON-STANDARD: This is for the password change page.
|
94 |
+
$this->option_pw_force_change_name = self::ID . '-pw-force-change-done';
|
95 |
+
$this->text_pw_force_change = __('Change All Passwords', self::ID);
|
96 |
+
$this->text_button_remind = __("Do not remind me about this", self::ID);
|
97 |
+
$this->text_button_require = __("Require All Passwords Be Changed", self::ID);
|
98 |
+
}
|
99 |
+
|
100 |
+
/*
|
101 |
+
* ===== ACTIVATION & DEACTIVATION CALLBACK METHODS =====
|
102 |
+
*/
|
103 |
+
|
104 |
+
/**
|
105 |
+
* Establishes the tables and settings when the plugin is activated
|
106 |
+
* @return void
|
107 |
+
*/
|
108 |
+
public function activate() {
|
109 |
+
global $wpdb;
|
110 |
+
|
111 |
+
/*
|
112 |
+
* Create or alter the plugin's tables as needed.
|
113 |
+
*/
|
114 |
+
|
115 |
+
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
116 |
+
|
117 |
+
// Note: dbDelta() requires two spaces after "PRIMARY KEY". Werid.
|
118 |
+
// WP's insert/prepare/etc don't handle NULL's (at least in 3.3).
|
119 |
+
$sql = "CREATE TABLE `$this->table_fail` (
|
120 |
+
fail_id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
121 |
+
ip VARCHAR(39) NOT NULL DEFAULT '',
|
122 |
+
user_login VARCHAR(60) NOT NULL DEFAULT '',
|
123 |
+
pass_md5 varchar(64) NOT NULL DEFAULT '',
|
124 |
+
date_failed TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
125 |
+
PRIMARY KEY (fail_id),
|
126 |
+
KEY (ip (9)),
|
127 |
+
KEY (user_login (5)),
|
128 |
+
KEY (pass_md5 (10))
|
129 |
+
)";
|
130 |
+
|
131 |
+
dbDelta($sql);
|
132 |
+
if ($wpdb->last_error) {
|
133 |
+
die($wpdb->last_error);
|
134 |
+
}
|
135 |
+
|
136 |
+
/*
|
137 |
+
* Save this plugin's options to the database.
|
138 |
+
*/
|
139 |
+
|
140 |
+
update_option($this->option_name, $this->options);
|
141 |
+
add_option($this->option_pw_force_change_name, 0, '', 'no');
|
142 |
+
|
143 |
+
/*
|
144 |
+
* Store password hashes.
|
145 |
+
*/
|
146 |
+
|
147 |
+
if ($this->options['pw_reuse_count']) {
|
148 |
+
$sql = "SELECT ID, user_pass FROM `$wpdb->users`";
|
149 |
+
$result = $wpdb->get_results($sql, ARRAY_A);
|
150 |
+
if (!$result) {
|
151 |
+
die(self::ID . ' could not find users.');
|
152 |
+
}
|
153 |
+
|
154 |
+
foreach ($result as $user) {
|
155 |
+
if (!$this->save_pw_hash($user['ID'], $user['user_pass'])) {
|
156 |
+
die(self::ID . ' could not save password hash.');
|
157 |
+
}
|
158 |
+
}
|
159 |
+
}
|
160 |
+
}
|
161 |
+
|
162 |
+
/**
|
163 |
+
* Removes the tables and settings when the plugin is deactivated
|
164 |
+
* if the deactivate_deletes_data option is turned on
|
165 |
+
* @return void
|
166 |
+
*/
|
167 |
+
public function deactivate() {
|
168 |
+
global $wpdb;
|
169 |
+
|
170 |
+
$prior_error_setting = $wpdb->show_errors;
|
171 |
+
$wpdb->show_errors = false;
|
172 |
+
$denied = 'command denied to user';
|
173 |
+
|
174 |
+
$wpdb->query("DROP TABLE `$this->table_fail`");
|
175 |
+
if ($wpdb->last_error) {
|
176 |
+
if (strpos($wpdb->last_error, $denied) === false) {
|
177 |
+
die($wpdb->last_error);
|
178 |
+
}
|
179 |
+
}
|
180 |
+
|
181 |
+
$wpdb->show_errors = $prior_error_setting;
|
182 |
+
|
183 |
+
$package_id = self::ID;
|
184 |
+
$wpdb->escape_by_ref($package_id);
|
185 |
+
|
186 |
+
$wpdb->query("DELETE FROM `$wpdb->options`
|
187 |
+
WHERE option_name LIKE '$package_id%'");
|
188 |
+
|
189 |
+
$wpdb->query("DELETE FROM `$wpdb->usermeta`
|
190 |
+
WHERE meta_key LIKE '$package_id%'");
|
191 |
+
}
|
192 |
+
|
193 |
+
/*
|
194 |
+
* ===== ADMIN USER INTERFACE =====
|
195 |
+
*/
|
196 |
+
|
197 |
+
/**
|
198 |
+
* Sets the metadata and labels for each element of the plugin's
|
199 |
+
* options
|
200 |
+
*
|
201 |
+
* @return void
|
202 |
+
* @uses login_security_solution_admin::$fields to hold the data
|
203 |
+
*/
|
204 |
+
protected function set_fields() {
|
205 |
+
$this->fields = array(
|
206 |
+
'idle_timeout' => array(
|
207 |
+
'group' => 'misc',
|
208 |
+
'label' => __("Idle Timeout", self::ID),
|
209 |
+
'text' => __("Close inactive sessions after this many minutes. 0 disables this feature.", self::ID),
|
210 |
+
'type' => 'int',
|
211 |
+
),
|
212 |
+
'disable_logins' => array(
|
213 |
+
'group' => 'misc',
|
214 |
+
'label' => __("Maintenance Mode", self::ID),
|
215 |
+
'text' => __("Disable logins from users who are not administrators and disable posting of comments?", self::ID),
|
216 |
+
'type' => 'bool',
|
217 |
+
'bool0' => __("Off, let all users log in.", self::ID),
|
218 |
+
'bool1' => __("On, disable comments and only let administrators log in.", self::ID),
|
219 |
+
),
|
220 |
+
'deactivate_deletes_data' => array(
|
221 |
+
'group' => 'misc',
|
222 |
+
'label' => __("Deactivation", self::ID),
|
223 |
+
'text' => __("Should deactivating the plugin remove all of the plugin's data and settings?", self::ID),
|
224 |
+
'type' => 'bool',
|
225 |
+
'bool0' => __("No, preserve the data for future use.", self::ID),
|
226 |
+
'bool1' => __("Yes, delete the damn data.", self::ID),
|
227 |
+
),
|
228 |
+
|
229 |
+
'login_fail_minutes' => array(
|
230 |
+
'group' => 'login',
|
231 |
+
'label' => __("Match Time", self::ID),
|
232 |
+
'text' => __("How far back, in minutes, should login failures look for matching data?", self::ID),
|
233 |
+
'type' => 'int',
|
234 |
+
),
|
235 |
+
'login_fail_tier_2' => array(
|
236 |
+
'group' => 'login',
|
237 |
+
'label' => __("Delay Tier 2", self::ID),
|
238 |
+
'text' => sprintf(__("How many matching login failures should it take to get into this (%d - %d second) Delay Tier? Must be >= %d.", self::ID), 4, 30, 2),
|
239 |
+
'type' => 'int',
|
240 |
+
'greater_than' => 2,
|
241 |
+
),
|
242 |
+
'login_fail_tier_3' => array(
|
243 |
+
'group' => 'login',
|
244 |
+
'label' => __("Delay Tier 3", self::ID),
|
245 |
+
'text' => sprintf(__("How many matching login failures should it take to get into this (%d - %d second) Delay Tier? Must be > Delay Tier 2.", self::ID), 25, 60),
|
246 |
+
'type' => 'int',
|
247 |
+
),
|
248 |
+
'login_fail_notify' => array(
|
249 |
+
'group' => 'login',
|
250 |
+
'label' => __("Failure Notification", self::ID),
|
251 |
+
'text' => __("Notify the administrator upon every x matching login failures. 0 disables this feature.", self::ID),
|
252 |
+
'type' => 'int',
|
253 |
+
),
|
254 |
+
'login_fail_breach_notify' => array(
|
255 |
+
'group' => 'login',
|
256 |
+
'label' => __("Breach Notification", self::ID),
|
257 |
+
'text' => __("Notify the administrator if a successful login uses data matching x login failures. 0 disables this feature.", self::ID),
|
258 |
+
'type' => 'int',
|
259 |
+
),
|
260 |
+
'login_fail_breach_pw_force_change' => array(
|
261 |
+
'group' => 'login',
|
262 |
+
'label' => __("Breach Email Confirm", self::ID),
|
263 |
+
'text' => __("If a successful login uses data matching x login failures, immediately log the user out and require them to use WordPress' lost password process. 0 disables this feature.", self::ID),
|
264 |
+
'type' => 'int',
|
265 |
+
),
|
266 |
+
|
267 |
+
'pw_length' => array(
|
268 |
+
'group' => 'pw',
|
269 |
+
'label' => __("Length", self::ID),
|
270 |
+
'text' => sprintf(__("How long must passwords be? Must be >= %d.", self::ID), 8),
|
271 |
+
'type' => 'int',
|
272 |
+
'greater_than' => 8,
|
273 |
+
),
|
274 |
+
'pw_complexity_exemption_length' => array(
|
275 |
+
'group' => 'pw',
|
276 |
+
'label' => __("Complexity Exemption", self::ID),
|
277 |
+
'text' => sprintf(__("How long must passwords be to be exempt from the complexity requirements? Must be >= %d.", self::ID), 20),
|
278 |
+
'type' => 'int',
|
279 |
+
'greater_than' => 20,
|
280 |
+
),
|
281 |
+
'pw_change_days' => array(
|
282 |
+
'group' => 'pw',
|
283 |
+
'label' => __("Aging", self::ID),
|
284 |
+
'text' => __("How many days old can a password be before requiring it be changed? Not recommended. 0 disables this feature.", self::ID),
|
285 |
+
'type' => 'int',
|
286 |
+
),
|
287 |
+
'pw_change_grace_period_minutes' => array(
|
288 |
+
'group' => 'pw',
|
289 |
+
'label' => __("Grace Period", self::ID),
|
290 |
+
'text' => sprintf(__("How many minutes should a user have to change their password once they know it has expired? Must be >= %d.", self::ID), 5),
|
291 |
+
'type' => 'int',
|
292 |
+
'greater_than' => 5,
|
293 |
+
),
|
294 |
+
'pw_reuse_count' => array(
|
295 |
+
'group' => 'pw',
|
296 |
+
'label' => __("History", self::ID),
|
297 |
+
'text' => __("How many passwords should be remembered? Prevents reuse of old passwords. 0 disables this feature.", self::ID),
|
298 |
+
'type' => 'int',
|
299 |
+
),
|
300 |
+
);
|
301 |
+
}
|
302 |
+
|
303 |
+
/**
|
304 |
+
* A filter to add a "Settings" link in this plugin's description
|
305 |
+
*
|
306 |
+
* NOTE: This method is automatically called by WordPress for each
|
307 |
+
* plugin being displayed on WordPress' Plugins admin page.
|
308 |
+
*
|
309 |
+
* @param array $links the links generated thus far
|
310 |
+
* @param string $file the name of the plugin currently being rendered
|
311 |
+
* @return array
|
312 |
+
*/
|
313 |
+
public function plugin_action_links($links, $file) {
|
314 |
+
if ($file == self::ID . '/' . self::ID . '.php') {
|
315 |
+
$links[] = '<a href="options-general.php?page='
|
316 |
+
. self::ID . '">' . __('Settings') . '</a>';
|
317 |
+
|
318 |
+
// NON-STANDARD: This is for the password change page.
|
319 |
+
$links[] = '<a href="options-general.php?page='
|
320 |
+
. $this->option_pw_force_change_name . '">'
|
321 |
+
. $this->text_pw_force_change . '</a>' ;
|
322 |
+
}
|
323 |
+
return $links;
|
324 |
+
}
|
325 |
+
|
326 |
+
/**
|
327 |
+
* Declares a menu item and callback for this plugin's settings page
|
328 |
+
*
|
329 |
+
* NOTE: This method is automatically called by WordPress when the
|
330 |
+
* any admin page is rendered
|
331 |
+
*/
|
332 |
+
public function admin_menu() {
|
333 |
+
add_options_page(
|
334 |
+
$this->text_settings,
|
335 |
+
self::NAME,
|
336 |
+
'activate_plugins',
|
337 |
+
self::ID,
|
338 |
+
array(&$this, 'page_settings')
|
339 |
+
);
|
340 |
+
}
|
341 |
+
|
342 |
+
/**
|
343 |
+
* Declares the callbacks for rendering and validating this plugin's
|
344 |
+
* settings sections and fields
|
345 |
+
*
|
346 |
+
* NOTE: This method is automatically called by WordPress when the
|
347 |
+
* any admin page is rendered
|
348 |
+
*/
|
349 |
+
public function admin_init() {
|
350 |
+
register_setting(
|
351 |
+
$this->option_name,
|
352 |
+
$this->option_name,
|
353 |
+
array(&$this, 'validate')
|
354 |
+
);
|
355 |
+
|
356 |
+
add_settings_section(
|
357 |
+
self::ID . '-login',
|
358 |
+
__('Login Failure Policies', self::ID),
|
359 |
+
array(&$this, 'section_login'),
|
360 |
+
self::ID
|
361 |
+
);
|
362 |
+
add_settings_section(
|
363 |
+
self::ID . '-pw',
|
364 |
+
__('Password Policies', self::ID),
|
365 |
+
array(&$this, 'section_blank'),
|
366 |
+
self::ID
|
367 |
+
);
|
368 |
+
add_settings_section(
|
369 |
+
self::ID . '-misc',
|
370 |
+
__('Miscellaneous Policies', self::ID),
|
371 |
+
array(&$this, 'section_blank'),
|
372 |
+
self::ID
|
373 |
+
);
|
374 |
+
|
375 |
+
// Dynamically declare each field using the info in $fields.
|
376 |
+
foreach ($this->fields as $id => $field) {
|
377 |
+
add_settings_field(
|
378 |
+
$id,
|
379 |
+
$field['label'],
|
380 |
+
array(&$this, $id),
|
381 |
+
self::ID,
|
382 |
+
self::ID . '-' . $field['group']
|
383 |
+
);
|
384 |
+
}
|
385 |
+
}
|
386 |
+
|
387 |
+
/**
|
388 |
+
* The callback for rendering the settings page
|
389 |
+
* @return void
|
390 |
+
*/
|
391 |
+
public function page_settings() {
|
392 |
+
echo '<h2>' . htmlspecialchars($this->text_settings) . '</h2>';
|
393 |
+
echo '<form action="options.php" method="post">' . "\n";
|
394 |
+
settings_fields($this->option_name);
|
395 |
+
do_settings_sections(self::ID);
|
396 |
+
submit_button();
|
397 |
+
echo '</form>';
|
398 |
+
}
|
399 |
+
|
400 |
+
/**
|
401 |
+
* The callback for "rendering" the sections that don't have text
|
402 |
+
* @return void
|
403 |
+
*/
|
404 |
+
public function section_blank() {
|
405 |
+
}
|
406 |
+
|
407 |
+
/**
|
408 |
+
* The callback for rendering the "Login Failures Policy" section
|
409 |
+
* @return void
|
410 |
+
*/
|
411 |
+
public function section_login() {
|
412 |
+
echo '<p>';
|
413 |
+
_e("This plugin stores the IP address, username and password for each failed log in attempt.", self::ID);
|
414 |
+
echo ' ';
|
415 |
+
_e("The data from future login failures are compared against the historical data.", self::ID);
|
416 |
+
echo ' ';
|
417 |
+
_e("If any of the data points match, the plugin delays printing out the failure message.", self::ID);
|
418 |
+
echo ' ';
|
419 |
+
_e("The goal is for the responses to take so long that the attackers give up and go find an easier target.", self::ID);
|
420 |
+
echo ' ';
|
421 |
+
_e("The length of the delay is broken up into three tiers.", self::ID);
|
422 |
+
echo ' ';
|
423 |
+
_e("The amount of the delay increases in higher tiers.", self::ID);
|
424 |
+
echo ' ';
|
425 |
+
_e("The delay time within each tier is randomized to complicate profiling by attackers.", self::ID);
|
426 |
+
echo '</p>';
|
427 |
+
}
|
428 |
+
|
429 |
+
/**
|
430 |
+
* The callback for rendering the fields
|
431 |
+
* @return void
|
432 |
+
*
|
433 |
+
* @uses login_security_solution_admin::input_radio() for rendering
|
434 |
+
* radio buttons
|
435 |
+
* @uses login_security_solution_admin::input_text() for rendering
|
436 |
+
* text input boxes
|
437 |
+
*/
|
438 |
+
public function __call($name, $params) {
|
439 |
+
if (empty($this->fields[$name]['type'])) {
|
440 |
+
return;
|
441 |
+
}
|
442 |
+
switch ($this->fields[$name]['type']) {
|
443 |
+
case 'bool':
|
444 |
+
$this->input_radio($name);
|
445 |
+
break;
|
446 |
+
case 'int':
|
447 |
+
$this->input_text($name);
|
448 |
+
break;
|
449 |
+
}
|
450 |
+
}
|
451 |
+
|
452 |
+
/**
|
453 |
+
* Renders the radio button inputs
|
454 |
+
* @return void
|
455 |
+
*/
|
456 |
+
protected function input_radio($name) {
|
457 |
+
echo htmlspecialchars($this->fields[$name]['text']) . '<br/>';
|
458 |
+
echo '<input type="radio" value="0" name="'
|
459 |
+
. htmlspecialchars($this->option_name)
|
460 |
+
. '[' . htmlspecialchars($name) . ']"'
|
461 |
+
. ($this->options[$name] ? '' : ' checked="checked"') . ' /> ';
|
462 |
+
echo htmlspecialchars($this->fields[$name]['bool0']);
|
463 |
+
echo '<br/>';
|
464 |
+
echo '<input type="radio" value="1" name="'
|
465 |
+
. htmlspecialchars($this->option_name)
|
466 |
+
. '[' . htmlspecialchars($name) . ']"'
|
467 |
+
. ($this->options[$name] ? ' checked="checked"' : '') . ' /> ';
|
468 |
+
echo htmlspecialchars($this->fields[$name]['bool1']);
|
469 |
+
}
|
470 |
+
|
471 |
+
/**
|
472 |
+
* Renders the text input boxes
|
473 |
+
* @return void
|
474 |
+
*/
|
475 |
+
protected function input_text($name) {
|
476 |
+
echo '<input type="text" size="3" name="'
|
477 |
+
. htmlspecialchars($this->option_name)
|
478 |
+
. '[' . htmlspecialchars($name) . ']"'
|
479 |
+
. ' value="' . htmlspecialchars($this->options[$name]) . '" /> ';
|
480 |
+
echo htmlspecialchars($this->fields[$name]['text']);
|
481 |
+
echo ' ' . __('Default:', self::ID) . ' '
|
482 |
+
. htmlspecialchars($this->options_default[$name]) . '.';
|
483 |
+
}
|
484 |
+
|
485 |
+
/**
|
486 |
+
* Validates the user input
|
487 |
+
*
|
488 |
+
* NOTE: WordPress saves the data even if this method says there are
|
489 |
+
* errors. So this method sets any inappropriate data to the default
|
490 |
+
* values.
|
491 |
+
*
|
492 |
+
* @param array $in the input submitted by the form
|
493 |
+
* @return array the sanitized data to be saved
|
494 |
+
*/
|
495 |
+
public function validate($in) {
|
496 |
+
$out = $this->options_default;
|
497 |
+
if (!is_array($in)) {
|
498 |
+
// Not translating this since only hackers will see it.
|
499 |
+
add_settings_error($this->option_name, $this->option_name,
|
500 |
+
'Input must be an array.');
|
501 |
+
return $out;
|
502 |
+
}
|
503 |
+
|
504 |
+
$gt_format = __("must be >= '%s',", self::ID);
|
505 |
+
$default = __("so we used the default value instead.", self::ID);
|
506 |
+
|
507 |
+
// Dynamically validate each field using the info in $fields.
|
508 |
+
foreach ($this->fields as $name => $field) {
|
509 |
+
if (!array_key_exists($name, $in)) {
|
510 |
+
continue;
|
511 |
+
}
|
512 |
+
|
513 |
+
if (!is_scalar($in[$name])) {
|
514 |
+
// Not translating this since only hackers will see it.
|
515 |
+
add_settings_error($this->option_name, $name, "'"
|
516 |
+
. htmlspecialchars($field['label'])
|
517 |
+
. "' was not a scalar, $default");
|
518 |
+
continue;
|
519 |
+
}
|
520 |
+
|
521 |
+
switch ($field['type']) {
|
522 |
+
case 'bool':
|
523 |
+
if ($in[$name] != 0 && $in[$name] != 1) {
|
524 |
+
// Not translating this since only hackers will see it.
|
525 |
+
add_settings_error($this->option_name, $name, "'"
|
526 |
+
. htmlspecialchars($field['label'])
|
527 |
+
. "' must be '0' or '1', $default");
|
528 |
+
continue 2;
|
529 |
+
}
|
530 |
+
break;
|
531 |
+
case 'int':
|
532 |
+
if (!ctype_digit($in[$name])) {
|
533 |
+
add_settings_error($this->option_name, $name, "'"
|
534 |
+
. htmlspecialchars($field['label'])
|
535 |
+
. "' " . __("must be an integer,", self::ID)
|
536 |
+
. ' ' . $default);
|
537 |
+
continue 2;
|
538 |
+
}
|
539 |
+
if (array_key_exists('greater_than', $field)
|
540 |
+
&& $in[$name] < $field['greater_than'])
|
541 |
+
{
|
542 |
+
add_settings_error($this->option_name, $name, "'"
|
543 |
+
. htmlspecialchars($field['label'])
|
544 |
+
. "' " . sprintf($gt_format,
|
545 |
+
$field['greater_than'])
|
546 |
+
. ' ' . $default);
|
547 |
+
continue 2;
|
548 |
+
}
|
549 |
+
break;
|
550 |
+
}
|
551 |
+
$out[$name] = $in[$name];
|
552 |
+
}
|
553 |
+
|
554 |
+
// Special check to make sure Delay Tier 3 > Delay Tier 2.
|
555 |
+
$name = 'login_fail_tier_3';
|
556 |
+
if ($out[$name] <= $out['login_fail_tier_2']) {
|
557 |
+
add_settings_error($this->option_name, $name, "'"
|
558 |
+
. htmlspecialchars($this->fields[$name]['label'])
|
559 |
+
. "' " . sprintf($gt_format,
|
560 |
+
htmlspecialchars($this->fields['login_fail_tier_2']['label']))
|
561 |
+
. ' ' . $default);
|
562 |
+
|
563 |
+
$out[$name] = $out['login_fail_tier_2'] + 5;
|
564 |
+
}
|
565 |
+
|
566 |
+
// Speical check to ensure reuse count is set if aging is enabled.
|
567 |
+
$name = 'pw_reuse_count';
|
568 |
+
if ($out['pw_change_days'] && !$out[$name]) {
|
569 |
+
add_settings_error($this->option_name, $name, "'"
|
570 |
+
. htmlspecialchars($this->fields[$name]['label'])
|
571 |
+
. "' " . sprintf($gt_format, 1)
|
572 |
+
. ' ' . $default);
|
573 |
+
|
574 |
+
$out[$name] = 5;
|
575 |
+
}
|
576 |
+
|
577 |
+
return $out;
|
578 |
+
}
|
579 |
+
|
580 |
+
/*
|
581 |
+
* ===== NON-STANDARD: ADMIN UI FOR FORCING PASSWORD CHANGES =====
|
582 |
+
*/
|
583 |
+
|
584 |
+
/**
|
585 |
+
* Declares a menu item and callback for the force password change page
|
586 |
+
*
|
587 |
+
* NOTE: This method is automatically called by WordPress when the
|
588 |
+
* any admin page is rendered
|
589 |
+
*/
|
590 |
+
public function admin_menu_pw_force_change() {
|
591 |
+
add_options_page(
|
592 |
+
$this->text_pw_force_change,
|
593 |
+
'',
|
594 |
+
'activate_plugins',
|
595 |
+
$this->option_pw_force_change_name,
|
596 |
+
array(&$this, 'page_pw_force_change')
|
597 |
+
);
|
598 |
+
}
|
599 |
+
|
600 |
+
/**
|
601 |
+
* Declares the callbacks for rendering and validating the
|
602 |
+
* force password change page
|
603 |
+
*
|
604 |
+
* NOTE: This method is automatically called by WordPress when the
|
605 |
+
* any admin page is rendered
|
606 |
+
*/
|
607 |
+
public function admin_init_pw_force_change() {
|
608 |
+
register_setting(
|
609 |
+
$this->option_pw_force_change_name,
|
610 |
+
$this->option_pw_force_change_name,
|
611 |
+
array(&$this, 'validate_pw_force_change')
|
612 |
+
);
|
613 |
+
|
614 |
+
add_settings_field(
|
615 |
+
'checkbox',
|
616 |
+
'',
|
617 |
+
array(&$this, 'field_blank'),
|
618 |
+
$this->option_pw_force_change_name
|
619 |
+
);
|
620 |
+
add_settings_field(
|
621 |
+
'submit',
|
622 |
+
'',
|
623 |
+
array(&$this, 'field_blank'),
|
624 |
+
$this->option_pw_force_change_name
|
625 |
+
);
|
626 |
+
}
|
627 |
+
|
628 |
+
/**
|
629 |
+
* The callback for rendering the force password change page
|
630 |
+
* @return void
|
631 |
+
*/
|
632 |
+
public function page_pw_force_change() {
|
633 |
+
echo '<h2>' . htmlspecialchars($this->text_pw_force_change) . '</h2>';
|
634 |
+
|
635 |
+
echo '<p>';
|
636 |
+
_e("There may be cases where everyone's password should be reset.", self::ID);
|
637 |
+
echo ' ';
|
638 |
+
printf(__("This page, provided by the %s plugin, offers that functionality.", self::ID), htmlspecialchars(self::NAME));
|
639 |
+
echo '</p>';
|
640 |
+
|
641 |
+
echo '<p>';
|
642 |
+
_e("Submitting this form sets a flag that forces all users to utilize WordPress' built in password reset functionality.", self::ID);
|
643 |
+
echo ' ';
|
644 |
+
_e("Users who are presently logged in will be logged out the next time they view a page that requires authentication.", self::ID);
|
645 |
+
echo '</p>';
|
646 |
+
|
647 |
+
echo '<form action="options.php" method="post">' . "\n";
|
648 |
+
settings_fields($this->option_pw_force_change_name);
|
649 |
+
|
650 |
+
$this->echo_div();
|
651 |
+
|
652 |
+
echo '<p><strong><input type="checkbox" value="1" name="'
|
653 |
+
. htmlspecialchars($this->option_pw_force_change_name)
|
654 |
+
. '[' . htmlspecialchars($this->key_checkbox_require)
|
655 |
+
. ']" /> ';
|
656 |
+
_e("Confirm that you want to force all users to change their passwords by checking this box, then click the button, below.", self::ID);
|
657 |
+
echo '</strong></p>';
|
658 |
+
|
659 |
+
submit_button(
|
660 |
+
$this->text_button_require,
|
661 |
+
'primary',
|
662 |
+
htmlspecialchars($this->option_pw_force_change_name) . '[submit]'
|
663 |
+
);
|
664 |
+
|
665 |
+
echo "</div>\n";
|
666 |
+
|
667 |
+
if (!$this->was_pw_force_change_done()) {
|
668 |
+
$this->echo_div();
|
669 |
+
|
670 |
+
echo '<p><input type="checkbox" value="1" name="'
|
671 |
+
. htmlspecialchars($this->option_pw_force_change_name)
|
672 |
+
. '[' . htmlspecialchars($this->key_checkbox_remind)
|
673 |
+
. ']" /> ';
|
674 |
+
_e("No thanks. I know what I'm doing. Please don't remind me about this.", self::ID);
|
675 |
+
echo '</p>';
|
676 |
+
|
677 |
+
submit_button(
|
678 |
+
$this->text_button_remind,
|
679 |
+
'secondary',
|
680 |
+
htmlspecialchars($this->option_pw_force_change_name) . '[submit]'
|
681 |
+
);
|
682 |
+
|
683 |
+
echo "</div>\n";
|
684 |
+
}
|
685 |
+
|
686 |
+
echo '</form>';
|
687 |
+
}
|
688 |
+
|
689 |
+
/**
|
690 |
+
* Receives the user input and calls the force_change() method
|
691 |
+
*
|
692 |
+
* @param array $in the input submitted by the form
|
693 |
+
* @return array an empty array
|
694 |
+
*
|
695 |
+
* @uses login_security_solution_admin::force_change_for_all() to flag
|
696 |
+
* everyone's account with the password change requirement
|
697 |
+
*/
|
698 |
+
public function validate_pw_force_change($in) {
|
699 |
+
$out = $this->was_pw_force_change_done();
|
700 |
+
|
701 |
+
if (is_array($in)
|
702 |
+
&& !empty($in['submit'])
|
703 |
+
&& is_scalar($in['submit']))
|
704 |
+
{
|
705 |
+
$crossed = __("You have checked a box that does not correspond with the button you pressed. Please check and press buttons inside the same section.", self::ID);
|
706 |
+
|
707 |
+
switch ($in['submit']) {
|
708 |
+
case $this->text_button_remind:
|
709 |
+
if (!empty($in[$this->key_checkbox_require])) {
|
710 |
+
add_settings_error($this->option_pw_force_change_name,
|
711 |
+
$this->option_pw_force_change_name, $crossed);
|
712 |
+
} elseif (empty($in[$this->key_checkbox_remind])) {
|
713 |
+
add_settings_error($this->option_pw_force_change_name, $this->option_pw_force_change_name, __("Please confirm that you really want to do this. Put a check in the 'No thanks' box before hitting the submit button.", self::ID));
|
714 |
+
} else {
|
715 |
+
// Translaton already in WP.
|
716 |
+
add_settings_error($this->option_pw_force_change_name,
|
717 |
+
$this->option_pw_force_change_name,
|
718 |
+
__('Success!'), 'updated');
|
719 |
+
$out = 1;
|
720 |
+
}
|
721 |
+
break;
|
722 |
+
case $this->text_button_require:
|
723 |
+
if (!empty($in[$this->key_checkbox_remind])) {
|
724 |
+
add_settings_error($this->option_pw_force_change_name,
|
725 |
+
$this->option_pw_force_change_name, $crossed);
|
726 |
+
} elseif (empty($in[$this->key_checkbox_require])) {
|
727 |
+
add_settings_error($this->option_pw_force_change_name, $this->option_pw_force_change_name, __("Please confirm that you really want to do this. Put a check in the 'Confirm' box before hitting the submit button.", self::ID));
|
728 |
+
} else {
|
729 |
+
$result = $this->force_change_for_all();
|
730 |
+
if ($result === true) {
|
731 |
+
// Translaton already in WP.
|
732 |
+
add_settings_error($this->option_pw_force_change_name,
|
733 |
+
$this->option_pw_force_change_name,
|
734 |
+
__('Success!'), 'updated');
|
735 |
+
$out = 1;
|
736 |
+
} else {
|
737 |
+
add_settings_error($this->option_pw_force_change_name,
|
738 |
+
$this->option_pw_force_change_name, $result);
|
739 |
+
}
|
740 |
+
}
|
741 |
+
break;
|
742 |
+
}
|
743 |
+
}
|
744 |
+
|
745 |
+
return $out;
|
746 |
+
}
|
747 |
+
|
748 |
+
/**
|
749 |
+
* Produces a notice at the top of each admin page, telling admins to
|
750 |
+
* run the Change All Passwords process
|
751 |
+
*
|
752 |
+
* NOTE: This method is automatically called by WordPress when the
|
753 |
+
* any admin page is rendered AND our Change All Passwords function
|
754 |
+
* has not been called.
|
755 |
+
*
|
756 |
+
* @return void
|
757 |
+
*/
|
758 |
+
public function admin_notices() {
|
759 |
+
if (!current_user_can('activate_plugins')) {
|
760 |
+
return;
|
761 |
+
}
|
762 |
+
|
763 |
+
echo '<div class="error">';
|
764 |
+
|
765 |
+
echo '<p><strong>';
|
766 |
+
_e("You have not asked your users to change their passwords since the plugin was activated. Most users have weak passwords. This plugin's password policies protect your site from brute force attacks. Please improve <em>everyone's</em> security by making all users pick new, strong, passwords.", self::ID);
|
767 |
+
echo '</strong></p>';
|
768 |
+
|
769 |
+
echo '<p><strong>';
|
770 |
+
_e("Speaking of which, do YOU have a strong password? Make sure by changing yours once you've submitted the Change All Passwords form.", self::ID);
|
771 |
+
echo '</strong></p>';
|
772 |
+
|
773 |
+
echo '<p><strong><a href="options-general.php?page=' . htmlspecialchars($this->option_pw_force_change_name) . '">' . $this->text_pw_force_change . "</a></strong></p>\n";
|
774 |
+
|
775 |
+
echo "</div>\n";
|
776 |
+
}
|
777 |
+
|
778 |
+
/**
|
779 |
+
* Produces a div tag for making borders
|
780 |
+
* @return void
|
781 |
+
*/
|
782 |
+
protected function echo_div() {
|
783 |
+
echo '<div style="margin: 0 1em 1em 0; border: thin solid black; padding: 0 1em;">';
|
784 |
+
}
|
785 |
+
|
786 |
+
/**
|
787 |
+
* Sets a user metadata key for each user in the database, requiring
|
788 |
+
* them to reset their passwords
|
789 |
+
* @return mixed true on success, string with error message on problem
|
790 |
+
*/
|
791 |
+
protected function force_change_for_all() {
|
792 |
+
global $user_ID, $wpdb;
|
793 |
+
|
794 |
+
if (!current_user_can('activate_plugins')) {
|
795 |
+
// Translaton already in WP.
|
796 |
+
return __('You do not have sufficient permissions to access this page.');
|
797 |
+
}
|
798 |
+
|
799 |
+
$sql = "INSERT INTO `$wpdb->usermeta`
|
800 |
+
(user_id, meta_key, meta_value)
|
801 |
+
SELECT ID, %s, 1
|
802 |
+
FROM `$wpdb->users`
|
803 |
+
LEFT JOIN `$wpdb->usermeta`
|
804 |
+
ON (`$wpdb->usermeta`.user_id
|
805 |
+
= `$wpdb->users`.ID
|
806 |
+
AND meta_key = %s)
|
807 |
+
WHERE meta_value IS NULL
|
808 |
+
AND `$wpdb->users`.ID <> %d";
|
809 |
+
|
810 |
+
$sql = $wpdb->prepare($sql, $this->umk_pw_force_change,
|
811 |
+
$this->umk_pw_force_change, $user_ID);
|
812 |
+
|
813 |
+
$wpdb->query($sql);
|
814 |
+
if ($wpdb->last_error) {
|
815 |
+
return $wpdb->last_error;
|
816 |
+
}
|
817 |
+
|
818 |
+
return true;
|
819 |
+
}
|
820 |
+
|
821 |
+
/**
|
822 |
+
* Gets the indicator of the status of whether the password
|
823 |
+
* change feature has been used after activation
|
824 |
+
*
|
825 |
+
* @return bool has the password change feature been used?
|
826 |
+
*/
|
827 |
+
protected function was_pw_force_change_done() {
|
828 |
+
return (bool) get_option($this->option_pw_force_change_name, false);
|
829 |
+
}
|
830 |
+
}
|
languages/login-security-solution.pot
ADDED
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Copyright (C) 2012 Login Security Solution
|
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.0.1\n"
|
6 |
+
"Report-Msgid-Bugs-To: http://wordpress.org/tag/login-security-solution\n"
|
7 |
+
"POT-Creation-Date: 2012-03-19 19:35:46+00:00\n"
|
8 |
+
"MIME-Version: 1.0\n"
|
9 |
+
"Content-Type: text/plain; charset=UTF-8\n"
|
10 |
+
"Content-Transfer-Encoding: 8bit\n"
|
11 |
+
"PO-Revision-Date: 2012-MO-DA HO:MI+ZONE\n"
|
12 |
+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
13 |
+
"Language-Team: LANGUAGE <LL@li.org>\n"
|
14 |
+
|
15 |
+
#: login-security-solution.php:422
|
16 |
+
msgid "Invalid username or password."
|
17 |
+
msgstr ""
|
18 |
+
|
19 |
+
#: login-security-solution.php:428 tests/LoginErrorsTest.php:110
|
20 |
+
#: tests/LoginErrorsTest.php:122
|
21 |
+
msgid "Password reset is not allowed for this user"
|
22 |
+
msgstr ""
|
23 |
+
|
24 |
+
#: login-security-solution.php:453 tests/LoginMessageTest.php:55
|
25 |
+
msgid "It has been over %d minutes since your last action."
|
26 |
+
msgstr ""
|
27 |
+
|
28 |
+
#: login-security-solution.php:454 tests/LoginMessageTest.php:56
|
29 |
+
msgid "Please log back in."
|
30 |
+
msgstr ""
|
31 |
+
|
32 |
+
#: login-security-solution.php:457 tests/LoginMessageTest.php:66
|
33 |
+
msgid "The grace period for changing your password has expired."
|
34 |
+
msgstr ""
|
35 |
+
|
36 |
+
#: login-security-solution.php:458 tests/LoginMessageTest.php:67
|
37 |
+
msgid "Please submit this form to reset your password."
|
38 |
+
msgstr ""
|
39 |
+
|
40 |
+
#: login-security-solution.php:461 tests/LoginMessageTest.php:77
|
41 |
+
msgid "Your password must be reset."
|
42 |
+
msgstr ""
|
43 |
+
|
44 |
+
#: login-security-solution.php:462 tests/LoginMessageTest.php:78
|
45 |
+
msgid "Please submit this form to reset it."
|
46 |
+
msgstr ""
|
47 |
+
|
48 |
+
#: login-security-solution.php:465 tests/LoginMessageTest.php:88
|
49 |
+
msgid "Your password has expired. Please log and change it."
|
50 |
+
msgstr ""
|
51 |
+
|
52 |
+
#: login-security-solution.php:466 tests/LoginMessageTest.php:89
|
53 |
+
msgid "We provide a %d minute grace period to do so."
|
54 |
+
msgstr ""
|
55 |
+
|
56 |
+
#: login-security-solution.php:472 tests/LoginMessageTest.php:103
|
57 |
+
#: tests/LoginMessageTest.php:118
|
58 |
+
msgid "The site is undergoing maintenance."
|
59 |
+
msgstr ""
|
60 |
+
|
61 |
+
#: login-security-solution.php:473 tests/LoginMessageTest.php:104
|
62 |
+
#: tests/LoginMessageTest.php:119
|
63 |
+
msgid "Please try again later."
|
64 |
+
msgstr ""
|
65 |
+
|
66 |
+
#: login-security-solution.php:532
|
67 |
+
msgid "<strong>ERROR</strong>: Passwords can not be reused."
|
68 |
+
msgstr ""
|
69 |
+
|
70 |
+
#: login-security-solution.php:785
|
71 |
+
msgid "Component Count Value from Current Attempt"
|
72 |
+
msgstr ""
|
73 |
+
|
74 |
+
#: login-security-solution.php:787
|
75 |
+
msgid "Network IP %5d %s"
|
76 |
+
msgstr ""
|
77 |
+
|
78 |
+
#: login-security-solution.php:789
|
79 |
+
msgid "Username %5d %s"
|
80 |
+
msgstr ""
|
81 |
+
|
82 |
+
#: login-security-solution.php:791
|
83 |
+
msgid "Password MD5 %5d %s"
|
84 |
+
msgstr ""
|
85 |
+
|
86 |
+
#: login-security-solution.php:1498
|
87 |
+
msgid "Your website, %s, may have been broken in to."
|
88 |
+
msgstr ""
|
89 |
+
|
90 |
+
#: login-security-solution.php:1501
|
91 |
+
msgid ""
|
92 |
+
"Someone just logged in using the following components. Prior to that, some "
|
93 |
+
"combination of those components were a part of %d failed attempts to log in "
|
94 |
+
"during the past %d minutes:"
|
95 |
+
msgstr ""
|
96 |
+
|
97 |
+
#: login-security-solution.php:1506
|
98 |
+
msgid ""
|
99 |
+
"The user has been logged out and will be required to confirm their identity "
|
100 |
+
"via the password reset functionality."
|
101 |
+
msgstr ""
|
102 |
+
|
103 |
+
#: login-security-solution.php:1535
|
104 |
+
msgid "Your website, %s, is undergoing a brute force attack."
|
105 |
+
msgstr ""
|
106 |
+
|
107 |
+
#: login-security-solution.php:1538
|
108 |
+
msgid ""
|
109 |
+
"There have been at least %d failed attempts to log in during the past %d "
|
110 |
+
"minutes that used one or more of the following components:"
|
111 |
+
msgstr ""
|
112 |
+
|
113 |
+
#: login-security-solution.php:1543
|
114 |
+
msgid ""
|
115 |
+
"The %s plugin for WordPress is repelling the attack by making their login "
|
116 |
+
"failures take a very long time."
|
117 |
+
msgstr ""
|
118 |
+
|
119 |
+
#: login-security-solution.php:1849 tests/PasswordValidationTest.php:436
|
120 |
+
msgid "<strong>ERROR</strong>: Password not set."
|
121 |
+
msgstr ""
|
122 |
+
|
123 |
+
#: login-security-solution.php:1864 tests/PasswordValidationTest.php:447
|
124 |
+
msgid "<strong>ERROR</strong>: Passwords must be strings."
|
125 |
+
msgstr ""
|
126 |
+
|
127 |
+
#: login-security-solution.php:1882 tests/PasswordValidationTest.php:460
|
128 |
+
msgid "<strong>ERROR</strong>: Passwords must use ASCII characters."
|
129 |
+
msgstr ""
|
130 |
+
|
131 |
+
#: login-security-solution.php:1901 tests/PasswordValidationTest.php:477
|
132 |
+
#: tests/PasswordValidationTest.php:491
|
133 |
+
msgid "<strong>ERROR</strong>: Password is too short."
|
134 |
+
msgstr ""
|
135 |
+
|
136 |
+
#: login-security-solution.php:1910 tests/PasswordValidationTest.php:517
|
137 |
+
msgid ""
|
138 |
+
"<strong>ERROR</strong>: Passwords must either contain numbers or be %d "
|
139 |
+
"characters long."
|
140 |
+
msgstr ""
|
141 |
+
|
142 |
+
#: login-security-solution.php:1919 tests/PasswordValidationTest.php:504
|
143 |
+
msgid ""
|
144 |
+
"<strong>ERROR</strong>: Passwords must either contain punctuation marks / "
|
145 |
+
"symbols or be %d characters long."
|
146 |
+
msgstr ""
|
147 |
+
|
148 |
+
#: login-security-solution.php:1928 tests/PasswordValidationTest.php:530
|
149 |
+
msgid ""
|
150 |
+
"<strong>ERROR</strong>: Passwords must either contain upper-case and lower-"
|
151 |
+
"case letters or be %d characters long."
|
152 |
+
msgstr ""
|
153 |
+
|
154 |
+
#: login-security-solution.php:1938 tests/PasswordValidationTest.php:543
|
155 |
+
msgid "<strong>ERROR</strong>: Passwords can't be sequential keys."
|
156 |
+
msgstr ""
|
157 |
+
|
158 |
+
#: login-security-solution.php:1947 tests/PasswordValidationTest.php:556
|
159 |
+
msgid ""
|
160 |
+
"<strong>ERROR</strong>: Passwords can't have that many sequential characters."
|
161 |
+
msgstr ""
|
162 |
+
|
163 |
+
#: login-security-solution.php:1963 tests/PasswordValidationTest.php:569
|
164 |
+
#: tests/PasswordValidationTest.php:582
|
165 |
+
msgid "<strong>ERROR</strong>: Passwords can't contain user data."
|
166 |
+
msgstr ""
|
167 |
+
|
168 |
+
#: login-security-solution.php:1974 tests/PasswordValidationTest.php:595
|
169 |
+
msgid "<strong>ERROR</strong>: Passwords can't contain site info."
|
170 |
+
msgstr ""
|
171 |
+
|
172 |
+
#: login-security-solution.php:1983 tests/PasswordValidationTest.php:608
|
173 |
+
msgid "<strong>ERROR</strong>: Password is too common."
|
174 |
+
msgstr ""
|
175 |
+
|
176 |
+
#: login-security-solution.php:1992 tests/PasswordValidationTest.php:621
|
177 |
+
msgid ""
|
178 |
+
"<strong>ERROR</strong>: Passwords can't be variations of dictionary words."
|
179 |
+
msgstr ""
|
180 |
+
|
181 |
+
#. Plugin Name of the plugin/theme
|
182 |
+
msgid "Login Security Solution"
|
183 |
+
msgstr ""
|
184 |
+
|
185 |
+
#. Plugin URI of the plugin/theme
|
186 |
+
msgid "http://wordpress.org/extend/plugins/login-security-solution/"
|
187 |
+
msgstr ""
|
188 |
+
|
189 |
+
#. Description of the plugin/theme
|
190 |
+
msgid ""
|
191 |
+
"Requires very strong passwords, repels brute force login attacks, prevents "
|
192 |
+
"login information disclosures, expires idle sessions, notifies admins of "
|
193 |
+
"attacks and breaches, permits administrators to disable logins for "
|
194 |
+
"maintenance or emergency reasons and reset all passwords."
|
195 |
+
msgstr ""
|
196 |
+
|
197 |
+
#. Author of the plugin/theme
|
198 |
+
msgid "Daniel Convissor"
|
199 |
+
msgstr ""
|
200 |
+
|
201 |
+
#. Author URI of the plugin/theme
|
202 |
+
msgid "http://www.analysisandsolutions.com/"
|
203 |
+
msgstr ""
|
login-security-solution.php
ADDED
@@ -0,0 +1,2001 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Plugin Name: Login Security Solution
|
5 |
+
*
|
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.0.4
|
10 |
+
* Author: Daniel Convissor
|
11 |
+
* Author URI: http://www.analysisandsolutions.com/
|
12 |
+
* License: GPLv2
|
13 |
+
* @package login-security-solution
|
14 |
+
*/
|
15 |
+
|
16 |
+
/**
|
17 |
+
* The instantiated version of this plugin's class
|
18 |
+
*/
|
19 |
+
$GLOBALS['login_security_solution'] = new login_security_solution;
|
20 |
+
|
21 |
+
/**
|
22 |
+
* The Login Security Solution plugin enhances WordPress' security
|
23 |
+
*
|
24 |
+
* @package login-security-solution
|
25 |
+
* @link http://wordpress.org/extend/plugins/login-security-solution/
|
26 |
+
* @version 0.0.1
|
27 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
28 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
29 |
+
* @copyright The Analysis and Solutions Company, 2012
|
30 |
+
*/
|
31 |
+
class login_security_solution {
|
32 |
+
/**
|
33 |
+
* This plugin's identifier
|
34 |
+
*/
|
35 |
+
const ID = 'login-security-solution';
|
36 |
+
|
37 |
+
/**
|
38 |
+
* This plugin's name
|
39 |
+
*/
|
40 |
+
const NAME = 'Login Security Solution';
|
41 |
+
|
42 |
+
/**
|
43 |
+
* This plugin's table name prefix
|
44 |
+
* @var string
|
45 |
+
*/
|
46 |
+
protected $prefix = 'login_security_solution_';
|
47 |
+
|
48 |
+
|
49 |
+
/**
|
50 |
+
* Is the dict command available?
|
51 |
+
* @var bool true/false if known, null if unknown
|
52 |
+
*/
|
53 |
+
protected $available_dict;
|
54 |
+
|
55 |
+
/**
|
56 |
+
* Is the grep command available?
|
57 |
+
* @var bool true/false if known, null if unknown
|
58 |
+
*/
|
59 |
+
protected $available_grep;
|
60 |
+
|
61 |
+
/**
|
62 |
+
* Is PHP's mbstring extension enabled?
|
63 |
+
* @var bool true/false if known, null if unknown
|
64 |
+
*/
|
65 |
+
protected $available_mbstring;
|
66 |
+
|
67 |
+
/**
|
68 |
+
* Location of our dictionary files
|
69 |
+
*
|
70 |
+
* Public for use by utilities.
|
71 |
+
*
|
72 |
+
* @var string
|
73 |
+
*/
|
74 |
+
public $dir_dictionaries;
|
75 |
+
|
76 |
+
/**
|
77 |
+
* Location of our sequence files
|
78 |
+
* @var string
|
79 |
+
*/
|
80 |
+
protected $dir_sequences;
|
81 |
+
|
82 |
+
/**
|
83 |
+
* Our URI query string key for passing messages to the login form
|
84 |
+
* @var string
|
85 |
+
*/
|
86 |
+
protected $key_login_msg;
|
87 |
+
|
88 |
+
/**
|
89 |
+
* Has the internationalization text domain been loaded?
|
90 |
+
* @var bool
|
91 |
+
*/
|
92 |
+
protected $loaded_textdomain = false;
|
93 |
+
|
94 |
+
/**
|
95 |
+
* This plugin's options
|
96 |
+
*
|
97 |
+
* Options from the database are merged on top of the default options.
|
98 |
+
*
|
99 |
+
* @see login_security_solution::set_options() to obtain the saved
|
100 |
+
* settings
|
101 |
+
* @var array
|
102 |
+
*/
|
103 |
+
protected $options = array();
|
104 |
+
|
105 |
+
/**
|
106 |
+
* This plugin's default options
|
107 |
+
* @var array
|
108 |
+
*/
|
109 |
+
protected $options_default = array(
|
110 |
+
'deactivate_deletes_data' => 0,
|
111 |
+
'disable_logins' => 0,
|
112 |
+
'idle_timeout' => 15,
|
113 |
+
'login_fail_minutes' => 120,
|
114 |
+
'login_fail_tier_2' => 5,
|
115 |
+
'login_fail_tier_3' => 10,
|
116 |
+
'login_fail_notify' => 20,
|
117 |
+
'login_fail_breach_notify' => 6,
|
118 |
+
'login_fail_breach_pw_force_change' => 6,
|
119 |
+
'pw_change_days' => 0,
|
120 |
+
'pw_change_grace_period_minutes' => 15,
|
121 |
+
'pw_complexity_exemption_length' => 20,
|
122 |
+
'pw_length' => 8,
|
123 |
+
'pw_reuse_count' => 0,
|
124 |
+
);
|
125 |
+
|
126 |
+
/**
|
127 |
+
* Our option name for storing the plugin's settings
|
128 |
+
* @var string
|
129 |
+
*/
|
130 |
+
protected $option_name;
|
131 |
+
|
132 |
+
/**
|
133 |
+
* Name, with $table_prefix, of the table tracking login failures
|
134 |
+
* @var string
|
135 |
+
*/
|
136 |
+
protected $table_fail;
|
137 |
+
|
138 |
+
/**
|
139 |
+
* Is this class being used by our unit tests?
|
140 |
+
* @var bool
|
141 |
+
*/
|
142 |
+
protected $testing = false;
|
143 |
+
|
144 |
+
/**
|
145 |
+
* Our usermeta key for tracking when passwords were changed
|
146 |
+
* @var string
|
147 |
+
*/
|
148 |
+
protected $umk_changed;
|
149 |
+
|
150 |
+
/**
|
151 |
+
* Our usermeta key for tracking when a password grace period started
|
152 |
+
* @var string
|
153 |
+
*/
|
154 |
+
protected $umk_grace_period;
|
155 |
+
|
156 |
+
/**
|
157 |
+
* Our usermeta key for tracking old passwords
|
158 |
+
* @var string
|
159 |
+
*/
|
160 |
+
protected $umk_hashes;
|
161 |
+
|
162 |
+
/**
|
163 |
+
* Our usermeta key for tracking when the user last hit the site
|
164 |
+
* @var string
|
165 |
+
*/
|
166 |
+
protected $umk_last_active;
|
167 |
+
|
168 |
+
|
169 |
+
/**
|
170 |
+
* Declares the WordPress action and filter callbacks
|
171 |
+
*
|
172 |
+
* @return void
|
173 |
+
* @uses login_security_solution::initialize() to set the object's
|
174 |
+
* properties
|
175 |
+
*/
|
176 |
+
public function __construct() {
|
177 |
+
$this->initialize();
|
178 |
+
|
179 |
+
add_action('auth_cookie_valid', array(&$this, 'check'), 1, 2);
|
180 |
+
add_action('password_reset', array(&$this, 'password_reset'), 10, 2);
|
181 |
+
add_action('user_profile_update_errors',
|
182 |
+
array(&$this, 'user_profile_update_errors'), 999, 3);
|
183 |
+
|
184 |
+
add_filter('login_errors', array(&$this, 'login_errors'));
|
185 |
+
add_filter('login_message', array(&$this, 'login_message'));
|
186 |
+
|
187 |
+
if ($this->options['disable_logins']) {
|
188 |
+
add_filter('comments_open', array(&$this, 'comments_open'));
|
189 |
+
}
|
190 |
+
|
191 |
+
if ($this->options['idle_timeout']) {
|
192 |
+
add_action('wp_login', array(&$this, 'delete_last_active'));
|
193 |
+
add_action('wp_logout', array(&$this, 'delete_last_active'));
|
194 |
+
}
|
195 |
+
|
196 |
+
if ($this->options['login_fail_breach_notify']
|
197 |
+
|| $this->options['login_fail_breach_pw_force_change'])
|
198 |
+
{
|
199 |
+
add_action('wp_login', array(&$this, 'wp_login'), 10, 2);
|
200 |
+
}
|
201 |
+
|
202 |
+
if (is_admin()) {
|
203 |
+
$this->load_plugin_textdomain();
|
204 |
+
|
205 |
+
require_once dirname(__FILE__) . '/admin.inc';
|
206 |
+
$admin = new login_security_solution_admin;
|
207 |
+
|
208 |
+
add_action('admin_menu', array(&$admin, 'admin_menu'));
|
209 |
+
add_action('admin_init', array(&$admin, 'admin_init'));
|
210 |
+
add_filter('plugin_action_links', array(&$admin, 'plugin_action_links'), 10, 2);
|
211 |
+
|
212 |
+
register_activation_hook(__FILE__, array(&$admin, 'activate'));
|
213 |
+
if ($this->options['deactivate_deletes_data']) {
|
214 |
+
register_deactivation_hook(__FILE__, array(&$admin, 'deactivate'));
|
215 |
+
}
|
216 |
+
|
217 |
+
// NON-STANDARD: This is for the password change page.
|
218 |
+
add_action('admin_menu', array(&$admin, 'admin_menu_pw_force_change'));
|
219 |
+
add_action('admin_init', array(&$admin, 'admin_init_pw_force_change'));
|
220 |
+
if (!$admin->was_pw_force_change_done()) {
|
221 |
+
add_action('admin_notices', array(&$admin, 'admin_notices'));
|
222 |
+
}
|
223 |
+
}
|
224 |
+
}
|
225 |
+
|
226 |
+
/**
|
227 |
+
* Sets the object's properties and options
|
228 |
+
*
|
229 |
+
* This is separated out from the constructor to avoid undesirable
|
230 |
+
* recursion. The constructor sometimes instantiates the admin class,
|
231 |
+
* which is a child of this class. So this method permits both the
|
232 |
+
* parent and child classes access to the settings and properties.
|
233 |
+
*
|
234 |
+
* @return void
|
235 |
+
*
|
236 |
+
* @uses login_security_solution::set_options() to replace the default
|
237 |
+
* options with those stored in the database
|
238 |
+
*/
|
239 |
+
protected function initialize() {
|
240 |
+
global $wpdb;
|
241 |
+
|
242 |
+
$this->table_fail = $wpdb->prefix . $this->prefix . 'fail';
|
243 |
+
|
244 |
+
$this->key_login_msg = self::ID . '-login-msg-id';
|
245 |
+
$this->option_name = self::ID . '-options';
|
246 |
+
$this->umk_changed = self::ID . '-pw-changed-time';
|
247 |
+
$this->umk_pw_force_change = self::ID . '-pw-force-change';
|
248 |
+
$this->umk_grace_period = self::ID . '-pw-grace-period-start-time';
|
249 |
+
$this->umk_hashes = self::ID . '-pw-hashes';
|
250 |
+
$this->umk_last_active = self::ID . '-last-active';
|
251 |
+
|
252 |
+
$this->dir_dictionaries = dirname(__FILE__) . '/pw_dictionaries/';
|
253 |
+
$this->dir_sequences = dirname(__FILE__) . '/pw_sequences/';
|
254 |
+
|
255 |
+
$this->set_options();
|
256 |
+
|
257 |
+
if ($this->options['login_fail_tier_2'] < 2) {
|
258 |
+
$this->options['login_fail_tier_2'] = 2;
|
259 |
+
}
|
260 |
+
if ($this->options['pw_change_days']
|
261 |
+
&& !$this->options['pw_reuse_count'])
|
262 |
+
{
|
263 |
+
$this->options['pw_reuse_count'] = 5;
|
264 |
+
}
|
265 |
+
if ($this->options['pw_change_grace_period_minutes'] < 5) {
|
266 |
+
$this->options['pw_change_grace_period_minutes'] = 5;
|
267 |
+
}
|
268 |
+
if ($this->options['pw_complexity_exemption_length'] < 20) {
|
269 |
+
$this->options['pw_complexity_exemption_length'] = 20;
|
270 |
+
}
|
271 |
+
if ($this->options['pw_length'] < 8) {
|
272 |
+
$this->options['pw_length'] = 8;
|
273 |
+
}
|
274 |
+
}
|
275 |
+
|
276 |
+
/*
|
277 |
+
* ===== ACTION & FILTER CALLBACK METHODS =====
|
278 |
+
*/
|
279 |
+
|
280 |
+
/**
|
281 |
+
* Redirects the current user to the login screen if their password
|
282 |
+
* is expired or needs to be reset
|
283 |
+
*
|
284 |
+
* NOTE: This method is automatically called by WordPress after
|
285 |
+
* successful validation of authentication cookies.
|
286 |
+
*
|
287 |
+
* @param array $cookie_elements values from the user's cookies
|
288 |
+
* @param WP_User $user the current user
|
289 |
+
* @return mixed return values provided for unit testing
|
290 |
+
*
|
291 |
+
* @uses login_security_solution::is_idle() to know if it has been too
|
292 |
+
* long since the user's last action
|
293 |
+
* @uses login_security_solution::is_pw_expired() to know if it has been
|
294 |
+
* too long since the password was last changed
|
295 |
+
* @uses login_security_solution::get_pw_force_change() to know if the
|
296 |
+
* user has to change their password for other reasons
|
297 |
+
* @uses login_security_solution::$options for the disable_logins setting
|
298 |
+
* @uses login_security_solution::redirect_to_login() to send the user to
|
299 |
+
* the login form and tell them what the problem is
|
300 |
+
*/
|
301 |
+
public function check($cookie_elements, $user) {
|
302 |
+
global $current_user;
|
303 |
+
|
304 |
+
// The auth_cookie_valid action may be executed multiple times.
|
305 |
+
// Bail if the current_user has not been determined yet.
|
306 |
+
if (!($current_user instanceof WP_User) || empty($user->ID)) {
|
307 |
+
return false;
|
308 |
+
}
|
309 |
+
|
310 |
+
/*
|
311 |
+
* NOTE: redirect_to_login() calls exit(), except when unit testing.
|
312 |
+
*/
|
313 |
+
|
314 |
+
if ($this->is_idle($user->ID)) {
|
315 |
+
$this->redirect_to_login('idle', true);
|
316 |
+
return -5;
|
317 |
+
}
|
318 |
+
|
319 |
+
if ($this->is_pw_expired($user->ID)) {
|
320 |
+
$grace = $this->check_pw_grace_period($user->ID);
|
321 |
+
if ($grace === true) {
|
322 |
+
// First time they've been here since password expired.
|
323 |
+
$this->redirect_to_login('pw_grace', true);
|
324 |
+
return -1;
|
325 |
+
} elseif ($grace === false) {
|
326 |
+
// Grace period has expired.
|
327 |
+
$this->redirect_to_login('pw_expired', false, 'retrievepassword');
|
328 |
+
return -2;
|
329 |
+
}
|
330 |
+
// Grace period is in effect, let them slide for now.
|
331 |
+
}
|
332 |
+
|
333 |
+
if ($this->get_pw_force_change($user->ID)) {
|
334 |
+
$this->redirect_to_login('pw_force', false, 'retrievepassword');
|
335 |
+
return -3;
|
336 |
+
}
|
337 |
+
|
338 |
+
if ($this->options['disable_logins']
|
339 |
+
&& !current_user_can('administrator'))
|
340 |
+
{
|
341 |
+
$this->redirect_to_login();
|
342 |
+
return -4;
|
343 |
+
}
|
344 |
+
|
345 |
+
return true;
|
346 |
+
}
|
347 |
+
|
348 |
+
/**
|
349 |
+
* Tells WordPress to disallow commenting on posts
|
350 |
+
*
|
351 |
+
* NOTE: This method is automatically called by WordPress when checking
|
352 |
+
* to see if comments are allowed on a post AND our "disable_logins"
|
353 |
+
* option is enabled
|
354 |
+
*
|
355 |
+
* @return bool always returns false
|
356 |
+
*/
|
357 |
+
public function comments_open() {
|
358 |
+
return false;
|
359 |
+
}
|
360 |
+
|
361 |
+
/**
|
362 |
+
* Removes the current user's last active time metadata
|
363 |
+
*
|
364 |
+
* NOTE: This method is automatically called by WordPress when users
|
365 |
+
* log in or out.
|
366 |
+
*
|
367 |
+
* @return bool|null null if $user_ID and $user_name are unknown
|
368 |
+
*/
|
369 |
+
public function delete_last_active() {
|
370 |
+
global $user_ID, $user_name;
|
371 |
+
|
372 |
+
if (empty($user_ID)) {
|
373 |
+
if (empty($user_name)) {
|
374 |
+
return;
|
375 |
+
}
|
376 |
+
$user = get_user_by('login', $user_name);
|
377 |
+
$user_ID = $user->ID;
|
378 |
+
}
|
379 |
+
|
380 |
+
return delete_user_meta($user_ID, $this->umk_last_active);
|
381 |
+
}
|
382 |
+
|
383 |
+
/**
|
384 |
+
* Alters the failure messages from logins and password resets that
|
385 |
+
* contain information disclosures
|
386 |
+
*
|
387 |
+
* The following measures are necessary, at least in WordPress 3.3:
|
388 |
+
* + Changes invalid user name message from log in process.
|
389 |
+
* + Changes invalid password message from log in process.
|
390 |
+
* + Unsets the user name when the password is wrong.
|
391 |
+
* + Changes invalid user name message from lost password process.
|
392 |
+
*
|
393 |
+
* These cloaking measures complicate cracking attempts by keeping
|
394 |
+
* attackers from knowing that half of the puzzle has been solved.
|
395 |
+
*
|
396 |
+
* NOTE: This method is automatically called by WordPress when attempted
|
397 |
+
* logins are unssucessful.
|
398 |
+
*
|
399 |
+
* @param string $out the output from earlier login_errors filters
|
400 |
+
* @return string
|
401 |
+
*
|
402 |
+
* @uses login_security_solution::process_login_fail() to log the failure
|
403 |
+
* and lock the user out if necessary
|
404 |
+
*/
|
405 |
+
public function login_errors($out = '') {
|
406 |
+
global $errors, $user_name;
|
407 |
+
|
408 |
+
if (isset($_REQUEST['action']) && $_REQUEST['action'] == 'register') {
|
409 |
+
// Do not alter "invalid_username" or "invalid_email" messages
|
410 |
+
// from registration process. (WP 3.3 reuses error codes.)
|
411 |
+
return $out;
|
412 |
+
}
|
413 |
+
|
414 |
+
$error_codes = $errors->get_error_codes();
|
415 |
+
|
416 |
+
$codes_to_cloak = array('incorrect_password', 'invalid_username');
|
417 |
+
if (array_intersect($error_codes, $codes_to_cloak)) {
|
418 |
+
unset($_POST['log']);
|
419 |
+
$user_pass = empty($_POST['pwd']) ? '' : $_POST['pwd'];
|
420 |
+
$this->process_login_fail($user_name, $user_pass);
|
421 |
+
$this->load_plugin_textdomain();
|
422 |
+
return __('Invalid username or password.', self::ID);
|
423 |
+
}
|
424 |
+
|
425 |
+
$codes_to_cloak = array('invalid_email', 'invalidcombo');
|
426 |
+
if (array_intersect($error_codes, $codes_to_cloak)) {
|
427 |
+
// This text is lifted directly from WordPress.
|
428 |
+
return __('Password reset is not allowed for this user');
|
429 |
+
}
|
430 |
+
|
431 |
+
return $out;
|
432 |
+
}
|
433 |
+
|
434 |
+
/**
|
435 |
+
* Adds our message to the other messages that appear above the login form
|
436 |
+
*
|
437 |
+
* NOTE: This method is automatically called by WordPress for displaying
|
438 |
+
* text above the login form.
|
439 |
+
*
|
440 |
+
* @param string $out the output from earlier login_message filters
|
441 |
+
* @return string
|
442 |
+
*
|
443 |
+
* @uses login_security_solution::$key_login_msg to know which $_GET
|
444 |
+
* parameter to watch for our message ID's
|
445 |
+
*/
|
446 |
+
public function login_message($out = '') {
|
447 |
+
$this->load_plugin_textdomain();
|
448 |
+
$ours = '';
|
449 |
+
|
450 |
+
if (!empty($_GET[$this->key_login_msg])) {
|
451 |
+
switch ($_GET[$this->key_login_msg]) {
|
452 |
+
case 'idle':
|
453 |
+
$ours = sprintf(__('It has been over %d minutes since your last action.', self::ID), $this->options['idle_timeout']);
|
454 |
+
$ours .= ' ' . __('Please log back in.', self::ID);
|
455 |
+
break;
|
456 |
+
case 'pw_expired':
|
457 |
+
$ours = __('The grace period for changing your password has expired.', self::ID);
|
458 |
+
$ours .= ' ' . __('Please submit this form to reset your password.', self::ID);
|
459 |
+
break;
|
460 |
+
case 'pw_force':
|
461 |
+
$ours = __('Your password must be reset.', self::ID);
|
462 |
+
$ours .= ' ' . __('Please submit this form to reset it.', self::ID);
|
463 |
+
break;
|
464 |
+
case 'pw_grace':
|
465 |
+
$ours = __('Your password has expired. Please log and change it.', self::ID);
|
466 |
+
$ours .= ' ' . sprintf(__('We provide a %d minute grace period to do so.', self::ID), $this->options['pw_change_grace_period_minutes']);
|
467 |
+
break;
|
468 |
+
}
|
469 |
+
}
|
470 |
+
|
471 |
+
if ($this->options['disable_logins']) {
|
472 |
+
$ours = __('The site is undergoing maintenance.', self::ID);
|
473 |
+
$ours .= ' ' . __('Please try again later.', self::ID);
|
474 |
+
}
|
475 |
+
|
476 |
+
if ($ours) {
|
477 |
+
$out .= '<p class="login message">'
|
478 |
+
. htmlspecialchars($ours) . '</p>';
|
479 |
+
}
|
480 |
+
|
481 |
+
return $out;
|
482 |
+
}
|
483 |
+
|
484 |
+
/**
|
485 |
+
* Conveys the password change information to the user's metadata
|
486 |
+
*
|
487 |
+
* NOTE: This method is automatically called by WordPress when users
|
488 |
+
* provide their new password via the password reset functionality.
|
489 |
+
*
|
490 |
+
* @param WP_User the user object being edited
|
491 |
+
* @param string $user_pass the unhashed new password
|
492 |
+
* @return void
|
493 |
+
*
|
494 |
+
* @uses login_security_solution::process_pw_metadata() to update user
|
495 |
+
* metadata
|
496 |
+
*/
|
497 |
+
public function password_reset($user, $user_pass) {
|
498 |
+
if (empty($user->ID)) {
|
499 |
+
return false;
|
500 |
+
}
|
501 |
+
$this->process_pw_metadata($user->ID, $user_pass);
|
502 |
+
}
|
503 |
+
|
504 |
+
/**
|
505 |
+
* Ensures passwords meet policy requirements
|
506 |
+
*
|
507 |
+
* NOTE: This method is automatically called by WordPress when users save
|
508 |
+
* their profile information or when admins add a user. The callback
|
509 |
+
* is activated in the edit_user() function in wp-admin/includes/user.php.
|
510 |
+
*
|
511 |
+
* @param WP_User the user object being edited
|
512 |
+
* @param bool $update is this an existing user?
|
513 |
+
* @param WP_Error the means to provide specific error messages
|
514 |
+
* @return bool|null return values provided for unit testing
|
515 |
+
*
|
516 |
+
* @uses login_security_solution::is_pw_reused() to know if it's an old
|
517 |
+
* pw
|
518 |
+
* @uses login_security_solution::validate_pw() to know if the pw is
|
519 |
+
* kosher
|
520 |
+
* @uses login_security_solution::process_pw_metadata() to update user
|
521 |
+
* metadata
|
522 |
+
*/
|
523 |
+
public function user_profile_update_errors(&$errors, $update, $user) {
|
524 |
+
if ($update) {
|
525 |
+
if (empty($user->user_pass) || empty($user->ID)) {
|
526 |
+
// Password is not being changed.
|
527 |
+
return null;
|
528 |
+
}
|
529 |
+
if ($this->is_pw_reused($user->user_pass, $user->ID)) {
|
530 |
+
$this->load_plugin_textdomain();
|
531 |
+
$errors->add(self::ID,
|
532 |
+
__("<strong>ERROR</strong>: Passwords can not be reused.", self::ID),
|
533 |
+
array('form-field' => 'pass1')
|
534 |
+
);
|
535 |
+
return false;
|
536 |
+
}
|
537 |
+
}
|
538 |
+
$answer = $this->validate_pw($user, $errors);
|
539 |
+
|
540 |
+
// Empty ID means an admin is adding a new user.
|
541 |
+
if (!empty($user->ID) && !$errors->get_error_codes()) {
|
542 |
+
$this->process_pw_metadata($user->ID, $user->user_pass);
|
543 |
+
}
|
544 |
+
|
545 |
+
return $answer;
|
546 |
+
}
|
547 |
+
|
548 |
+
/**
|
549 |
+
* Removes the current user's last active time metadata
|
550 |
+
*
|
551 |
+
* NOTE: This method is automatically called by WordPress when users
|
552 |
+
* successfully log in.
|
553 |
+
*
|
554 |
+
* @param string $user_name the user name from the current login form
|
555 |
+
* @param WP_User $user the current user
|
556 |
+
* @return mixed return values provided for unit testing
|
557 |
+
*
|
558 |
+
* @uses login_security_solution::get_network_ip() gets the IP's
|
559 |
+
* "network" part
|
560 |
+
* @uses login_security_solution::md5() to hash the password
|
561 |
+
* @uses login_security_solution::get_login_fail() to see if
|
562 |
+
* they're over the limit
|
563 |
+
* @uses login_security_solution::$options for the
|
564 |
+
* login_fail_breach_notify value
|
565 |
+
* @uses login_security_solution::$options for the
|
566 |
+
* login_fail_breach_pw_force_change value
|
567 |
+
* @uses login_security_solution::set_pw_force_change() to keep atackers
|
568 |
+
* from doing damage or changing the account's email address
|
569 |
+
* @uses login_security_solution::notify_breach() to warn of the breach
|
570 |
+
*/
|
571 |
+
public function wp_login($user_name, $user) {
|
572 |
+
if (!$user_name) {
|
573 |
+
return;
|
574 |
+
}
|
575 |
+
if (!$this->options['login_fail_breach_notify']
|
576 |
+
&& !$this->options['login_fail_breach_pw_force_change'])
|
577 |
+
{
|
578 |
+
return -1;
|
579 |
+
}
|
580 |
+
|
581 |
+
$network_ip = $this->get_network_ip();
|
582 |
+
$pass_md5 = $this->md5(empty($_POST['pwd']) ? '' : $_POST['pwd']);
|
583 |
+
|
584 |
+
$return = 1;
|
585 |
+
$fails = $this->get_login_fail($network_ip, $user_name, $pass_md5);
|
586 |
+
|
587 |
+
if ($this->options['login_fail_breach_pw_force_change']
|
588 |
+
&& $fails['total'] >= $this->options['login_fail_breach_pw_force_change'])
|
589 |
+
{
|
590 |
+
$this->set_pw_force_change($user->ID);
|
591 |
+
$return += 2;
|
592 |
+
}
|
593 |
+
|
594 |
+
if ($this->options['login_fail_breach_notify']
|
595 |
+
&& $fails['total'] >= $this->options['login_fail_breach_notify'])
|
596 |
+
{
|
597 |
+
$this->notify_breach($network_ip, $user_name, $pass_md5, $fails);
|
598 |
+
$return += 4;
|
599 |
+
}
|
600 |
+
|
601 |
+
return $return;
|
602 |
+
}
|
603 |
+
|
604 |
+
/*
|
605 |
+
* ===== INTERNAL METHODS ====
|
606 |
+
*/
|
607 |
+
|
608 |
+
/**
|
609 |
+
* Examines and manipulates password grace periods as needed
|
610 |
+
*
|
611 |
+
* @param int $user_ID the current user's ID number
|
612 |
+
* @return mixed true if the grace period just started, integer of
|
613 |
+
* minutes remaining if in effect, false if exceeded
|
614 |
+
*
|
615 |
+
* @uses login_security_solution::get_pw_grace_period() to know the grace
|
616 |
+
* period starting time
|
617 |
+
* @uses login_security_solution::set_pw_grace_period() to set the grace
|
618 |
+
* period starting time if it does not exist
|
619 |
+
* @uses login_security_solution::$options for the
|
620 |
+
* pw_change_grace_period_minutes setting
|
621 |
+
*/
|
622 |
+
protected function check_pw_grace_period($user_ID) {
|
623 |
+
$start = $this->get_pw_grace_period($user_ID);
|
624 |
+
if (!$start) {
|
625 |
+
$this->set_pw_grace_period($user_ID);
|
626 |
+
return true;
|
627 |
+
}
|
628 |
+
|
629 |
+
$remaining = $start - time()
|
630 |
+
+ ($this->options['pw_change_grace_period_minutes'] * 60);
|
631 |
+
|
632 |
+
if ($remaining < 0) {
|
633 |
+
return false;
|
634 |
+
}
|
635 |
+
return $remaining;
|
636 |
+
}
|
637 |
+
|
638 |
+
/**
|
639 |
+
* Changes commonly used transpositions into their actual equivalents
|
640 |
+
*
|
641 |
+
* @param string $pw the string to clean up
|
642 |
+
* @return string the human readable string
|
643 |
+
*/
|
644 |
+
protected function convert_leet_speak($pw) {
|
645 |
+
$leet = array('!', '@', '$', '+', '1', '3', '4', '5', '6', '9', '0');
|
646 |
+
$normal = array('i', 'a', 's', 't', 'l', 'e', 'a', 's', 'b', 'g', 'o');
|
647 |
+
return str_replace($leet, $normal, $pw);
|
648 |
+
}
|
649 |
+
|
650 |
+
/**
|
651 |
+
* Remove's the "force password change" flag from the user's metadata
|
652 |
+
*
|
653 |
+
* @param int $user_ID the current user's ID number
|
654 |
+
* @return bool
|
655 |
+
*/
|
656 |
+
protected function delete_pw_force_change($user_ID) {
|
657 |
+
return delete_user_meta($user_ID, $this->umk_pw_force_change);
|
658 |
+
}
|
659 |
+
|
660 |
+
/**
|
661 |
+
* Remove's the "password grace period" from the user's metadata
|
662 |
+
*
|
663 |
+
* @param int $user_ID the current user's ID number
|
664 |
+
* @return bool
|
665 |
+
*/
|
666 |
+
protected function delete_pw_grace_period($user_ID) {
|
667 |
+
return delete_user_meta($user_ID, $this->umk_grace_period);
|
668 |
+
}
|
669 |
+
|
670 |
+
/**
|
671 |
+
* Obtains the IP address from $_SERVER['REMOTE_ADDR']
|
672 |
+
*
|
673 |
+
* Also performs basic sanity checks on the addresses.
|
674 |
+
*
|
675 |
+
* @return string the IP address. Empty string if input is bad.
|
676 |
+
*
|
677 |
+
* @uses login_security_solution::normalize_ip() to clean up addresses
|
678 |
+
*/
|
679 |
+
protected function get_ip() {
|
680 |
+
if (empty($_SERVER['REMOTE_ADDR'])) {
|
681 |
+
return '';
|
682 |
+
}
|
683 |
+
|
684 |
+
return $this->normalize_ip($_SERVER['REMOTE_ADDR']);
|
685 |
+
}
|
686 |
+
|
687 |
+
/**
|
688 |
+
* Obtains the timestamp of the given user's last hit on the site
|
689 |
+
*
|
690 |
+
* @param int $user_ID the current user's ID number
|
691 |
+
* @return int the Unix timestamp of the user's last hit
|
692 |
+
*/
|
693 |
+
protected function get_last_active($user_ID) {
|
694 |
+
return (int) get_user_meta($user_ID, $this->umk_last_active, true);
|
695 |
+
}
|
696 |
+
|
697 |
+
/**
|
698 |
+
* Obtains the number of login failures for the current IP, user name
|
699 |
+
* and password in the period specified by login_fail_minutes
|
700 |
+
*
|
701 |
+
* @param string $network_ip a prior result from get_network_ip()
|
702 |
+
* @param string $user_name the user name from the current login form
|
703 |
+
* @param string $pass_md5 the md5 hashed new password
|
704 |
+
* @return array an associative array with the details
|
705 |
+
*
|
706 |
+
* @uses login_security_solution::$options for the login_fail_minutes
|
707 |
+
* setting
|
708 |
+
*/
|
709 |
+
protected function get_login_fail($network_ip, $user_name, $pass_md5) {
|
710 |
+
global $wpdb;
|
711 |
+
|
712 |
+
$wpdb->escape_by_ref($user_name);
|
713 |
+
$wpdb->escape_by_ref($pass_md5);
|
714 |
+
|
715 |
+
if ($network_ip) {
|
716 |
+
// Can't use wpdb::prepare() because it adds quote marks.
|
717 |
+
$wpdb->escape_by_ref($network_ip);
|
718 |
+
$ip_search = "ip LIKE '$network_ip%'";
|
719 |
+
} else {
|
720 |
+
$ip_search = "ip = ''";
|
721 |
+
}
|
722 |
+
|
723 |
+
$sql = "SELECT COUNT(*) AS total,
|
724 |
+
SUM(IF($ip_search, 1, 0)) AS network_ip,
|
725 |
+
SUM(IF(user_login = '$user_name', 1, 0)) AS user_name,
|
726 |
+
SUM(IF(pass_md5 = '$pass_md5', 1, 0)) AS pass_md5
|
727 |
+
FROM `$this->table_fail`
|
728 |
+
WHERE (ip LIKE '$network_ip%'
|
729 |
+
OR user_login = '$user_name'
|
730 |
+
OR pass_md5 = '$pass_md5')
|
731 |
+
AND date_failed > DATE_SUB(NOW(), INTERVAL "
|
732 |
+
. (int) $this->options['login_fail_minutes'] . " MINUTE)";
|
733 |
+
|
734 |
+
return $wpdb->get_row($sql, ARRAY_A);
|
735 |
+
}
|
736 |
+
|
737 |
+
/**
|
738 |
+
* Gets the "network" component of an IP address
|
739 |
+
*
|
740 |
+
* The "network" component for IPv4 is the first three groups ("Class C")
|
741 |
+
* while for IPv6 it is the first four groups.
|
742 |
+
*
|
743 |
+
* WARNING: This method performs no validation because the data comes
|
744 |
+
* from get_ip() which has already performed sanity checks.
|
745 |
+
*
|
746 |
+
* @param string $ip a prior result from get_ip(). Defaults to
|
747 |
+
* $_SERVER['REMOTE_ADDR'].
|
748 |
+
*
|
749 |
+
* @return string the IP address. Empty string if input is bad.
|
750 |
+
*
|
751 |
+
* @uses login_security_solution::get_ip() to get the
|
752 |
+
* $_SERVER['REMOTE_ADDR']
|
753 |
+
*/
|
754 |
+
protected function get_network_ip($ip = '') {
|
755 |
+
if (!$ip) {
|
756 |
+
$ip = $this->get_ip();
|
757 |
+
if (!$ip) {
|
758 |
+
return $ip;
|
759 |
+
}
|
760 |
+
}
|
761 |
+
|
762 |
+
if (!is_string($ip)) {
|
763 |
+
return '';
|
764 |
+
}
|
765 |
+
|
766 |
+
if (strpos($ip, ':') === false) {
|
767 |
+
return substr($ip, 0, strrpos($ip, '.'));
|
768 |
+
} else {
|
769 |
+
$groups = explode(':', $ip);
|
770 |
+
return implode(':', array_intersect_key($groups, array(0, 1, 2, 3)));
|
771 |
+
}
|
772 |
+
}
|
773 |
+
|
774 |
+
/**
|
775 |
+
* Produces text for use in the notify messages
|
776 |
+
*
|
777 |
+
* @param string $network_ip a prior result from get_network_ip()
|
778 |
+
* @param string $user_name the user name from the current login form
|
779 |
+
* @param string $pass_md5 the md5 hashed new password
|
780 |
+
* @return string
|
781 |
+
*/
|
782 |
+
protected function get_notify_counts($network_ip, $user_name, $pass_md5,
|
783 |
+
$fails)
|
784 |
+
{
|
785 |
+
return __("Component Count Value from Current Attempt", self::ID)
|
786 |
+
. "\n------------ ----- --------------------------------\n"
|
787 |
+
. sprintf(__("Network IP %5d %s", self::ID),
|
788 |
+
$fails['network_ip'], $network_ip) . "\n"
|
789 |
+
. sprintf(__("Username %5d %s", self::ID),
|
790 |
+
$fails['user_name'], $user_name) . "\n"
|
791 |
+
. sprintf(__("Password MD5 %5d %s", self::ID),
|
792 |
+
$fails['pass_md5'], $pass_md5) . "\n\n";
|
793 |
+
}
|
794 |
+
|
795 |
+
/**
|
796 |
+
* Obtains the timestamp of when the user last changed their password
|
797 |
+
*
|
798 |
+
* @param int $user_ID the current user's ID number
|
799 |
+
* @return int the Unix timestamp of the user's last password change
|
800 |
+
*/
|
801 |
+
protected function get_pw_changed_time($user_ID) {
|
802 |
+
return (int) get_user_meta($user_ID, $this->umk_changed, true);
|
803 |
+
}
|
804 |
+
|
805 |
+
/**
|
806 |
+
* Reads the "force password change" flag from the user's metadata
|
807 |
+
*
|
808 |
+
* @param int $user_ID the current user's ID number
|
809 |
+
* @return bool does the user need to change their password?
|
810 |
+
*/
|
811 |
+
protected function get_pw_force_change($user_ID) {
|
812 |
+
return (bool) get_user_meta($user_ID, $this->umk_pw_force_change, true);
|
813 |
+
}
|
814 |
+
|
815 |
+
/**
|
816 |
+
* Obtains the timestamp of when the user's "password grace period"
|
817 |
+
* started
|
818 |
+
*
|
819 |
+
* @param int $user_ID the current user's ID number
|
820 |
+
* @return int the Unix timestamp of the user's grace period beginning
|
821 |
+
*/
|
822 |
+
protected function get_pw_grace_period($user_ID) {
|
823 |
+
return (int) get_user_meta($user_ID, $this->umk_grace_period, true);
|
824 |
+
}
|
825 |
+
|
826 |
+
/**
|
827 |
+
* Obtains the password hashes from the user's metadata
|
828 |
+
*
|
829 |
+
* @param int $user_ID the current user's ID number
|
830 |
+
* @return array the user's existing pasword hashes
|
831 |
+
*/
|
832 |
+
protected function get_pw_hashes($user_ID) {
|
833 |
+
$hashes = get_user_meta($user_ID, $this->umk_hashes, true);
|
834 |
+
if (empty($hashes)) {
|
835 |
+
$hashes = array();
|
836 |
+
} elseif (!is_array($hashes)) {
|
837 |
+
$hashes = (array) $hashes;
|
838 |
+
}
|
839 |
+
return $hashes;
|
840 |
+
}
|
841 |
+
|
842 |
+
/**
|
843 |
+
* Does the password or given string use the same text?
|
844 |
+
*
|
845 |
+
* @param string $pw the password to examine
|
846 |
+
* @param string $string the string to compare the password against
|
847 |
+
* @return bool
|
848 |
+
*/
|
849 |
+
protected function has_match($pw, $string) {
|
850 |
+
if (!is_string($string)) {
|
851 |
+
return false;
|
852 |
+
}
|
853 |
+
$string = trim($string);
|
854 |
+
if (!$string) {
|
855 |
+
return false;
|
856 |
+
}
|
857 |
+
if (stripos($pw, $string) !== false) {
|
858 |
+
return true;
|
859 |
+
}
|
860 |
+
if (stripos($string, $pw) !== false) {
|
861 |
+
return true;
|
862 |
+
}
|
863 |
+
return false;
|
864 |
+
}
|
865 |
+
|
866 |
+
/**
|
867 |
+
* Saves the failed login's info in the database
|
868 |
+
*
|
869 |
+
* @param string $ip a prior result from get_ip()
|
870 |
+
* @param string $user_name the user name from the current login form
|
871 |
+
* @param string $pass_md5 the md5 hashed new password
|
872 |
+
* @return void
|
873 |
+
*/
|
874 |
+
protected function insert_fail($ip, $user_login, $pass_md5) {
|
875 |
+
global $wpdb;
|
876 |
+
|
877 |
+
$wpdb->insert(
|
878 |
+
$this->table_fail,
|
879 |
+
array(
|
880 |
+
'ip' => $ip,
|
881 |
+
'user_login' => $user_login,
|
882 |
+
'pass_md5' => $pass_md5,
|
883 |
+
),
|
884 |
+
array('%s', '%s', '%s')
|
885 |
+
);
|
886 |
+
}
|
887 |
+
|
888 |
+
/**
|
889 |
+
* Examines how long ago the current user last interacted with the
|
890 |
+
* site and takes appropriate action
|
891 |
+
*
|
892 |
+
* @param int $user_ID the user's id number
|
893 |
+
* @return mixed true if idle. Other replies all evaluate to empty
|
894 |
+
* but use different types to aid unit testing.
|
895 |
+
*
|
896 |
+
* @uses login_security_solution::$options for the idle_timeout value
|
897 |
+
* @uses login_security_solution::get_last_active() for the user's last
|
898 |
+
* hit time
|
899 |
+
* @uses login_security_solution::set_last_active() to update the user's
|
900 |
+
* time
|
901 |
+
*/
|
902 |
+
public function is_idle($user_ID) {
|
903 |
+
if (!$this->options['idle_timeout']) {
|
904 |
+
return null;
|
905 |
+
}
|
906 |
+
|
907 |
+
$last_active = $this->get_last_active($user_ID);
|
908 |
+
if (!$last_active) {
|
909 |
+
$this->set_last_active($user_ID);
|
910 |
+
return 0;
|
911 |
+
}
|
912 |
+
|
913 |
+
if (($this->options['idle_timeout'] * 60) + $last_active < time()) {
|
914 |
+
return true;
|
915 |
+
}
|
916 |
+
|
917 |
+
$this->set_last_active($user_ID);
|
918 |
+
|
919 |
+
return false;
|
920 |
+
}
|
921 |
+
|
922 |
+
/**
|
923 |
+
* Does this password show up in the "dict" program?
|
924 |
+
*
|
925 |
+
* @param string $pw the password to examine
|
926 |
+
* @return bool|null true or false if known, null if dict isn't available
|
927 |
+
*/
|
928 |
+
protected function is_pw_dict_program($pw) {
|
929 |
+
if ($this->available_dict === false) {
|
930 |
+
return null;
|
931 |
+
}
|
932 |
+
|
933 |
+
$term = escapeshellarg($pw);
|
934 |
+
exec("dict -m -s exact $term 2>&1", $output, $result);
|
935 |
+
if (!$result) {
|
936 |
+
return true;
|
937 |
+
} elseif ($result == 127) {
|
938 |
+
$this->available_dict = false;
|
939 |
+
return null;
|
940 |
+
}
|
941 |
+
return false;
|
942 |
+
}
|
943 |
+
|
944 |
+
/**
|
945 |
+
* Is this password in our dictionary files?
|
946 |
+
*
|
947 |
+
* The checks are done using "grep." If grep is not available, each file
|
948 |
+
* is examined using file() and in_array().
|
949 |
+
*
|
950 |
+
* The dictionary files are in the "pw_dictionaries" directory. Feel free
|
951 |
+
* to add your own dictionary files. Please be aware that checking the
|
952 |
+
* files is computationally "expensive" and the larger the files become,
|
953 |
+
* the more time and memory is needed. Thus it is wise to only put
|
954 |
+
* passwords your files that would not be caught by our other tests.
|
955 |
+
* The "utilties/reduce-dictionary-files.php" script can be used to
|
956 |
+
* weed out unnecessary entries.
|
957 |
+
*
|
958 |
+
* @param string $pw the password to examine
|
959 |
+
* @return bool
|
960 |
+
*/
|
961 |
+
protected function is_pw_dictionary($pw) {
|
962 |
+
if ($this->available_grep === true) {
|
963 |
+
return $this->is_pw_dictionary__grep($pw);
|
964 |
+
} elseif ($this->available_grep === false) {
|
965 |
+
return $this->is_pw_dictionary__file($pw);
|
966 |
+
}
|
967 |
+
$result = $this->is_pw_dictionary__grep($pw);
|
968 |
+
if ($result !== null) {
|
969 |
+
return $result;
|
970 |
+
}
|
971 |
+
return $this->is_pw_dictionary__file($pw);
|
972 |
+
}
|
973 |
+
|
974 |
+
/**
|
975 |
+
* Examines the password files via file() and in_array()
|
976 |
+
*
|
977 |
+
* @param string $pw the password to examine
|
978 |
+
* @return bool
|
979 |
+
*/
|
980 |
+
protected function is_pw_dictionary__file($pw) {
|
981 |
+
$dir = new DirectoryIterator($this->dir_dictionaries);
|
982 |
+
foreach ($dir as $file) {
|
983 |
+
if ($file->isDir()) {
|
984 |
+
continue;
|
985 |
+
}
|
986 |
+
$words = file($this->dir_dictionaries . $file->getFilename(),
|
987 |
+
FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
|
988 |
+
if (in_array($pw, $words)) {
|
989 |
+
return true;
|
990 |
+
}
|
991 |
+
}
|
992 |
+
return false;
|
993 |
+
}
|
994 |
+
|
995 |
+
/**
|
996 |
+
* Examines the password files via grep, if it is available
|
997 |
+
*
|
998 |
+
* @param string $pw the password to examine
|
999 |
+
* @return bool|null true or false if known, null if grep isn't available
|
1000 |
+
*/
|
1001 |
+
protected function is_pw_dictionary__grep($pw) {
|
1002 |
+
if ($this->available_grep === false) {
|
1003 |
+
return null;
|
1004 |
+
}
|
1005 |
+
|
1006 |
+
$term = escapeshellarg($pw);
|
1007 |
+
$dir = escapeshellarg($this->dir_dictionaries);
|
1008 |
+
exec("grep -iqrx $term $dir", $output, $result);
|
1009 |
+
if (!$result) {
|
1010 |
+
return true;
|
1011 |
+
} elseif ($result == 127) {
|
1012 |
+
$this->available_grep = false;
|
1013 |
+
return null;
|
1014 |
+
}
|
1015 |
+
return false;
|
1016 |
+
}
|
1017 |
+
|
1018 |
+
/**
|
1019 |
+
* Is the user's password expired?
|
1020 |
+
*
|
1021 |
+
* @param int $user_ID the user's id number
|
1022 |
+
* @return mixed true if expired. Other replies all evaluate to empty
|
1023 |
+
* but use different types to aid unit testing.
|
1024 |
+
*
|
1025 |
+
* @uses login_security_solution::$options for the pw_change_days value
|
1026 |
+
* @uses login_security_solution::get_last_changed_time() to get the last
|
1027 |
+
* time the user changed their password
|
1028 |
+
* @uses login_security_solution::set_last_changed_time() to update the
|
1029 |
+
* user's password changed time if it's not available
|
1030 |
+
*/
|
1031 |
+
protected function is_pw_expired($user_ID) {
|
1032 |
+
if (!$this->options['pw_change_days']) {
|
1033 |
+
return null;
|
1034 |
+
}
|
1035 |
+
$time = $this->get_pw_changed_time($user_ID);
|
1036 |
+
if (!$time) {
|
1037 |
+
$this->set_pw_changed_time($user_ID);
|
1038 |
+
return 0;
|
1039 |
+
}
|
1040 |
+
if (((time() - $time) / 86400) > $this->options['pw_change_days']) {
|
1041 |
+
return true;
|
1042 |
+
}
|
1043 |
+
return false;
|
1044 |
+
}
|
1045 |
+
|
1046 |
+
/**
|
1047 |
+
* Does the password use the site's name, url or description?
|
1048 |
+
*
|
1049 |
+
* @param string $pw the password to examine
|
1050 |
+
* @return bool
|
1051 |
+
*/
|
1052 |
+
protected function is_pw_like_bloginfo($pw) {
|
1053 |
+
// Note: avoiding get_bloginfo() because it's very expensive.
|
1054 |
+
if ($this->has_match($pw, get_option('blogname'))) {
|
1055 |
+
return true;
|
1056 |
+
}
|
1057 |
+
if ($this->has_match($pw, get_option('siteurl'))) {
|
1058 |
+
return true;
|
1059 |
+
}
|
1060 |
+
if ($this->has_match($pw, get_option('blogdescription'))) {
|
1061 |
+
return true;
|
1062 |
+
}
|
1063 |
+
return false;
|
1064 |
+
}
|
1065 |
+
|
1066 |
+
/**
|
1067 |
+
* Does the password contain data from the user's profile?
|
1068 |
+
*
|
1069 |
+
* @param string $pw the password to examine
|
1070 |
+
* @return bool
|
1071 |
+
*/
|
1072 |
+
protected function is_pw_like_user_data($pw, $user) {
|
1073 |
+
if (!empty($user->user_login)) {
|
1074 |
+
if ($this->has_match($pw, $user->user_login)) {
|
1075 |
+
return true;
|
1076 |
+
}
|
1077 |
+
}
|
1078 |
+
if (!empty($user->user_email)) {
|
1079 |
+
if ($this->has_match($pw, $user->user_email)) {
|
1080 |
+
return true;
|
1081 |
+
}
|
1082 |
+
}
|
1083 |
+
if (!empty($user->user_url)) {
|
1084 |
+
if ($this->has_match($pw, $user->user_url)) {
|
1085 |
+
return true;
|
1086 |
+
}
|
1087 |
+
}
|
1088 |
+
if (!empty($user->first_name)) {
|
1089 |
+
if ($this->has_match($pw, $user->first_name)) {
|
1090 |
+
return true;
|
1091 |
+
}
|
1092 |
+
}
|
1093 |
+
if (!empty($user->last_name)) {
|
1094 |
+
if ($this->has_match($pw, $user->last_name)) {
|
1095 |
+
return true;
|
1096 |
+
}
|
1097 |
+
}
|
1098 |
+
if (!empty($user->nickname)) {
|
1099 |
+
if ($this->has_match($pw, $user->nickname)) {
|
1100 |
+
return true;
|
1101 |
+
}
|
1102 |
+
}
|
1103 |
+
if (!empty($user->display_name)) {
|
1104 |
+
if ($this->has_match($pw, $user->display_name)) {
|
1105 |
+
return true;
|
1106 |
+
}
|
1107 |
+
}
|
1108 |
+
if (!empty($user->aim)) {
|
1109 |
+
if ($this->has_match($pw, $user->aim)) {
|
1110 |
+
return true;
|
1111 |
+
}
|
1112 |
+
}
|
1113 |
+
if (!empty($user->yim)) {
|
1114 |
+
if ($this->has_match($pw, $user->yim)) {
|
1115 |
+
return true;
|
1116 |
+
}
|
1117 |
+
}
|
1118 |
+
if (!empty($user->jabber)) {
|
1119 |
+
if ($this->has_match($pw, $user->jabber)) {
|
1120 |
+
return true;
|
1121 |
+
}
|
1122 |
+
}
|
1123 |
+
return false;
|
1124 |
+
}
|
1125 |
+
|
1126 |
+
/**
|
1127 |
+
* Does the password lack numbers?
|
1128 |
+
*
|
1129 |
+
* @param string $pw the password to examine
|
1130 |
+
* @return bool
|
1131 |
+
*/
|
1132 |
+
protected function is_pw_missing_numeric($pw) {
|
1133 |
+
return !preg_match('/\d/u', $pw);
|
1134 |
+
}
|
1135 |
+
|
1136 |
+
/**
|
1137 |
+
* Does the password lack punctuation characters?
|
1138 |
+
*
|
1139 |
+
* @param string $pw the password to examine
|
1140 |
+
* @return bool
|
1141 |
+
*/
|
1142 |
+
protected function is_pw_missing_punct_chars($pw) {
|
1143 |
+
return !preg_match('/[^\p{L}\p{Nd}]/u', $pw);
|
1144 |
+
}
|
1145 |
+
|
1146 |
+
/**
|
1147 |
+
* Does the password lack upper-case letters and lower-case letters?
|
1148 |
+
*
|
1149 |
+
* @param string $pw the password to examine
|
1150 |
+
* @return bool
|
1151 |
+
*/
|
1152 |
+
protected function is_pw_missing_upper_lower_chars($pw) {
|
1153 |
+
if ($this->available_mbstring) {
|
1154 |
+
$upper = mb_strtoupper($pw);
|
1155 |
+
$lower = mb_strtolower($pw);
|
1156 |
+
if ($upper == $lower) {
|
1157 |
+
if (preg_match('/^[\P{L}\p{Nd}]+$/u', $pw)) {
|
1158 |
+
// Contains only numbers or punctuation. Sorry, Charlie.
|
1159 |
+
return true;
|
1160 |
+
}
|
1161 |
+
// Unicameral alphabet. That's cool.
|
1162 |
+
return false;
|
1163 |
+
}
|
1164 |
+
if ($pw != $lower && $pw != $upper) {
|
1165 |
+
return false;
|
1166 |
+
}
|
1167 |
+
return true;
|
1168 |
+
} else {
|
1169 |
+
if (!preg_match('/[[:upper:]]/u', $pw)) {
|
1170 |
+
return true;
|
1171 |
+
}
|
1172 |
+
if (!preg_match('/[[:lower:]]/u', $pw)) {
|
1173 |
+
return true;
|
1174 |
+
}
|
1175 |
+
return false;
|
1176 |
+
}
|
1177 |
+
}
|
1178 |
+
|
1179 |
+
/**
|
1180 |
+
* Does the password contain things other than ASCII characters?
|
1181 |
+
*
|
1182 |
+
* @param string $pw the password to examine
|
1183 |
+
* @return bool
|
1184 |
+
*/
|
1185 |
+
protected function is_pw_outside_ascii($pw) {
|
1186 |
+
return !preg_match('/^[!-~]+$/u', $pw);
|
1187 |
+
}
|
1188 |
+
|
1189 |
+
/**
|
1190 |
+
* Is the user's password the same as one they've used earlier?
|
1191 |
+
*
|
1192 |
+
* @param string $pw the password to examine
|
1193 |
+
* @return mixed true if reused. Other replies all evaluate to empty
|
1194 |
+
* but use different types to aid unit testing.
|
1195 |
+
*/
|
1196 |
+
protected function is_pw_reused($pw, $user_ID) {
|
1197 |
+
if (!$this->options['pw_reuse_count']) {
|
1198 |
+
return null;
|
1199 |
+
}
|
1200 |
+
$hashes = $this->get_pw_hashes($user_ID);
|
1201 |
+
if (empty($hashes)) {
|
1202 |
+
return 0;
|
1203 |
+
}
|
1204 |
+
foreach ($hashes as $hash) {
|
1205 |
+
if (wp_check_password($pw, $hash)) {
|
1206 |
+
return true;
|
1207 |
+
}
|
1208 |
+
}
|
1209 |
+
return false;
|
1210 |
+
}
|
1211 |
+
|
1212 |
+
/**
|
1213 |
+
* Does the password contain characters in alphabetic or numeric order?
|
1214 |
+
*
|
1215 |
+
* @param string $pw the password to examine
|
1216 |
+
* @return bool
|
1217 |
+
*/
|
1218 |
+
protected function is_pw_sequential_codepoints($pw) {
|
1219 |
+
$chars = $this->split($pw);
|
1220 |
+
$prior = array_shift($chars);
|
1221 |
+
$transitions = 0;
|
1222 |
+
foreach ($chars as $char) {
|
1223 |
+
if (abs( hexdec(bin2hex($char)) - hexdec(bin2hex($prior)) ) > 2) {
|
1224 |
+
$transitions++;
|
1225 |
+
}
|
1226 |
+
$prior = $char;
|
1227 |
+
}
|
1228 |
+
return ($transitions < 5);
|
1229 |
+
}
|
1230 |
+
|
1231 |
+
/**
|
1232 |
+
* Does the password contain groups of characters next to each other
|
1233 |
+
* on the keyboard?
|
1234 |
+
*
|
1235 |
+
* This method uses files stored in the "pw_sequences" directory. Each
|
1236 |
+
* file represents a different keyboard/language. The files are created
|
1237 |
+
* (for left-to-right languages) by typing each character on the keyboard
|
1238 |
+
* starting with the top left key, working across the top row, then
|
1239 |
+
* starting again on the left side of the next row down. Do the full
|
1240 |
+
* keyboard in upper-case mode first. Then continue by doing the board
|
1241 |
+
* in lower-case mode. Feel free to add your own files.
|
1242 |
+
*
|
1243 |
+
* @param string $pw the password to examine
|
1244 |
+
* @return bool
|
1245 |
+
*/
|
1246 |
+
protected function is_pw_sequential_file($pw) {
|
1247 |
+
// First, determine offsets where character type changes occur.
|
1248 |
+
$split = preg_split('/(?<=[^[:punct:]])([[:punct:]])|(?<=[^[:alpha:]])([[:alpha:]])|(?<=\D)(\d)/', $pw, -1, PREG_SPLIT_OFFSET_CAPTURE|PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
|
1249 |
+
|
1250 |
+
if (count($split) == 1) {
|
1251 |
+
// All one character type.
|
1252 |
+
$parts_fwd = array($pw);
|
1253 |
+
$parts_rev = array($this->strrev($pw));
|
1254 |
+
} else {
|
1255 |
+
// Multiple character types.
|
1256 |
+
|
1257 |
+
// Don't want info from first element.
|
1258 |
+
array_shift($split);
|
1259 |
+
|
1260 |
+
$parts_fwd = array();
|
1261 |
+
$parts_rev = array();
|
1262 |
+
$start = 0;
|
1263 |
+
|
1264 |
+
// Now use those offsets to extract the character type blocks.
|
1265 |
+
foreach ($split as $part) {
|
1266 |
+
if ($this->strlen($part[0]) == 1) {
|
1267 |
+
$length = $part[1] - $start;
|
1268 |
+
if ($length > 2) {
|
1269 |
+
// Only examine blocks with 3 or more characters.
|
1270 |
+
$fwd = $this->substr($pw, $start, $length);
|
1271 |
+
$parts_fwd[] = $fwd;
|
1272 |
+
$parts_rev[] = $this->strrev($fwd);
|
1273 |
+
}
|
1274 |
+
$start = $part[1];
|
1275 |
+
}
|
1276 |
+
}
|
1277 |
+
$length = $this->strlen($pw) - $start;
|
1278 |
+
if ($length > 2) {
|
1279 |
+
// Only add the last block if it's 3 or more characters.
|
1280 |
+
$fwd = $this->substr($pw, $start, $length);
|
1281 |
+
$parts_fwd[] = $fwd;
|
1282 |
+
$parts_rev[] = $this->strrev($fwd);
|
1283 |
+
}
|
1284 |
+
}
|
1285 |
+
|
1286 |
+
if (!$parts_fwd) {
|
1287 |
+
return false;
|
1288 |
+
}
|
1289 |
+
|
1290 |
+
$dir = new DirectoryIterator($this->dir_sequences);
|
1291 |
+
foreach ($dir as $file) {
|
1292 |
+
if ($file->isDir()) {
|
1293 |
+
continue;
|
1294 |
+
}
|
1295 |
+
$kbd = file_get_contents($this->dir_sequences . $file->getFileName());
|
1296 |
+
|
1297 |
+
foreach ($parts_fwd as $key => $part) {
|
1298 |
+
if ($this->strlen($part) < 3) {
|
1299 |
+
continue;
|
1300 |
+
}
|
1301 |
+
if (strpos($kbd, $part) !== false) {
|
1302 |
+
return true;
|
1303 |
+
}
|
1304 |
+
if (strpos($kbd, $parts_rev[$key]) !== false) {
|
1305 |
+
return true;
|
1306 |
+
}
|
1307 |
+
}
|
1308 |
+
}
|
1309 |
+
return false;
|
1310 |
+
}
|
1311 |
+
|
1312 |
+
/**
|
1313 |
+
* A centralized way to load the plugin's textdomain for
|
1314 |
+
* internationalization
|
1315 |
+
* @return void
|
1316 |
+
*/
|
1317 |
+
protected function load_plugin_textdomain() {
|
1318 |
+
if (!$this->loaded_textdomain) {
|
1319 |
+
load_plugin_textdomain(self::ID, false, self::ID . '/languages');
|
1320 |
+
$this->loaded_textdomain = true;
|
1321 |
+
}
|
1322 |
+
}
|
1323 |
+
|
1324 |
+
/**
|
1325 |
+
* Sends a message to my debug log
|
1326 |
+
*/
|
1327 |
+
public function log($msg) {
|
1328 |
+
if (!is_scalar($msg)) {
|
1329 |
+
$msg = var_export($msg, true);
|
1330 |
+
}
|
1331 |
+
file_put_contents('/var/tmp/' . self::ID . '.log', "$msg\n", FILE_APPEND);
|
1332 |
+
}
|
1333 |
+
|
1334 |
+
/**
|
1335 |
+
* Generates a reproducible hash of the password
|
1336 |
+
*
|
1337 |
+
* Needed because WP's hash function creates different output each time,
|
1338 |
+
* making it impossible to search against.
|
1339 |
+
*
|
1340 |
+
* @param string $pw the password to process
|
1341 |
+
* @return string the hashed password
|
1342 |
+
*
|
1343 |
+
* @uses AUTH_SALT to prevent rainbow table lookups
|
1344 |
+
*/
|
1345 |
+
protected function md5($pw) {
|
1346 |
+
return md5(AUTH_SALT . $pw);
|
1347 |
+
}
|
1348 |
+
|
1349 |
+
/**
|
1350 |
+
* Formats and sanity checks IP addresses
|
1351 |
+
*
|
1352 |
+
* @param string $ip the IP address to check
|
1353 |
+
* @return string the formatted address. Empty string if input is bad.
|
1354 |
+
*/
|
1355 |
+
protected function normalize_ip($ip) {
|
1356 |
+
if (!is_string($ip)) {
|
1357 |
+
return '';
|
1358 |
+
}
|
1359 |
+
$ip = trim($ip);
|
1360 |
+
if ($ip == '') {
|
1361 |
+
return $ip;
|
1362 |
+
}
|
1363 |
+
if (strpos($ip, ':') === false) {
|
1364 |
+
return $this->normalize_ipv4($ip);
|
1365 |
+
} else {
|
1366 |
+
return $this->normalize_ipv6($ip);
|
1367 |
+
}
|
1368 |
+
}
|
1369 |
+
|
1370 |
+
/**
|
1371 |
+
* Userland means for sanity checking IPv4 addresses
|
1372 |
+
*
|
1373 |
+
* @param string $ip the IPv4 address, in "." separated format
|
1374 |
+
* @return string the IP address. Empty string if input is bad.
|
1375 |
+
*/
|
1376 |
+
protected function normalize_ipv4($ip) {
|
1377 |
+
$groups = explode('.', $ip);
|
1378 |
+
if (count($groups) != 4) {
|
1379 |
+
return '';
|
1380 |
+
}
|
1381 |
+
$out = array();
|
1382 |
+
foreach ($groups as $group) {
|
1383 |
+
$group = (int) $group;
|
1384 |
+
if ($group > 255) {
|
1385 |
+
return '';
|
1386 |
+
}
|
1387 |
+
$out[] = $group;
|
1388 |
+
}
|
1389 |
+
return implode('.', $out);
|
1390 |
+
}
|
1391 |
+
|
1392 |
+
/**
|
1393 |
+
* Fills in compressed groups, providing a consistent format usable for
|
1394 |
+
* wildcard searching
|
1395 |
+
*
|
1396 |
+
* Also performs sanity checks.
|
1397 |
+
*
|
1398 |
+
* The output does not comply with RFC 5952 because compressed addresses
|
1399 |
+
* can cause mistakes in our "LIKE '$network_ip%'" queries.
|
1400 |
+
*
|
1401 |
+
* @link http://tools.ietf.org/html/rfc5952 A Recommendation for IPv6
|
1402 |
+
* Address Text Representation
|
1403 |
+
*
|
1404 |
+
* @param string $ip the IPv6 address, in ":" separated format
|
1405 |
+
* @return string the formatted address. Empty string if input is bad.
|
1406 |
+
*/
|
1407 |
+
protected function normalize_ipv6($ip) {
|
1408 |
+
if (strpos($ip, ':::') !== false || $ip == '::') {
|
1409 |
+
return '';
|
1410 |
+
}
|
1411 |
+
|
1412 |
+
$groups = explode(':', $ip);
|
1413 |
+
|
1414 |
+
$compression_location = strpos($ip, '::');
|
1415 |
+
if ($compression_location === 0) {
|
1416 |
+
array_shift($groups);
|
1417 |
+
} elseif ($compression_location == strlen($ip) - 2) {
|
1418 |
+
array_pop($groups);
|
1419 |
+
}
|
1420 |
+
|
1421 |
+
$count = count($groups);
|
1422 |
+
|
1423 |
+
if ($count > 8) {
|
1424 |
+
return '';
|
1425 |
+
}
|
1426 |
+
if ($count < 8) {
|
1427 |
+
if (strpos($groups[$count -1], '.') !== false) {
|
1428 |
+
// Embedded IPv4.
|
1429 |
+
$prior = hexdec($groups[$count - 2]);
|
1430 |
+
if ($prior == 0 || $prior == 65535) {
|
1431 |
+
$ipv4 = $this->normalize_ipv4($groups[$count - 1]);
|
1432 |
+
if ($ipv4) {
|
1433 |
+
if ($prior) {
|
1434 |
+
return '0:0:0:0:0:ffff:' . $ipv4;
|
1435 |
+
} else {
|
1436 |
+
return '0:0:0:0:0:0:' . $ipv4;
|
1437 |
+
}
|
1438 |
+
}
|
1439 |
+
}
|
1440 |
+
return '';
|
1441 |
+
}
|
1442 |
+
if ($compression_location === false) {
|
1443 |
+
return '';
|
1444 |
+
}
|
1445 |
+
}
|
1446 |
+
|
1447 |
+
$out = array();
|
1448 |
+
$missing = 9 - $count;
|
1449 |
+
foreach ($groups as $key => $value) {
|
1450 |
+
if ($value === '') {
|
1451 |
+
$out = array_merge($out, array_fill(0, $missing, '0'));
|
1452 |
+
} else {
|
1453 |
+
// Ensure no leading 0's and that values are legit.
|
1454 |
+
if (ctype_digit($value)) {
|
1455 |
+
$value = (int) $value;
|
1456 |
+
if ($value > 9999) {
|
1457 |
+
return '';
|
1458 |
+
}
|
1459 |
+
} else {
|
1460 |
+
$tmp = hexdec($value);
|
1461 |
+
if ($tmp > 65535) {
|
1462 |
+
return '';
|
1463 |
+
}
|
1464 |
+
$value = dechex($tmp);
|
1465 |
+
}
|
1466 |
+
$out[] = $value;
|
1467 |
+
}
|
1468 |
+
}
|
1469 |
+
|
1470 |
+
$ip = implode(':', $out);
|
1471 |
+
return $ip;
|
1472 |
+
}
|
1473 |
+
|
1474 |
+
/**
|
1475 |
+
* Sends an email to the blog's administrator telling them a breakin
|
1476 |
+
* may have occurred
|
1477 |
+
*
|
1478 |
+
* @param string $network_ip a prior result from get_network_ip()
|
1479 |
+
* @param string $user_name the user name from the current login form
|
1480 |
+
* @param string $pass_md5 the md5 hashed new password
|
1481 |
+
* @return bool
|
1482 |
+
*
|
1483 |
+
* @uses login_security_solution::get_notify_counts() for some shared text
|
1484 |
+
* @uses wp_mail() to send the messages
|
1485 |
+
*/
|
1486 |
+
protected function notify_breach($network_ip, $user_name, $pass_md5,
|
1487 |
+
$fails)
|
1488 |
+
{
|
1489 |
+
$this->load_plugin_textdomain();
|
1490 |
+
|
1491 |
+
$to = $this->sanitize_whitespace(get_option('admin_email'));
|
1492 |
+
|
1493 |
+
$blog = get_option('blogname');
|
1494 |
+
$subject = sprintf('POTENTIAL INTRUSION AT %s', $blog);
|
1495 |
+
$subject = $this->sanitize_whitespace($subject);
|
1496 |
+
|
1497 |
+
$message =
|
1498 |
+
sprintf(__("Your website, %s, may have been broken in to.", self::ID),
|
1499 |
+
$blog) . "\n\n"
|
1500 |
+
|
1501 |
+
. 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),
|
1502 |
+
$fails['total'], $this->options['login_fail_minutes']) . "\n\n"
|
1503 |
+
|
1504 |
+
. $this->get_notify_counts($network_ip, $user_name, $pass_md5, $fails)
|
1505 |
+
|
1506 |
+
. __("The user has been logged out and will be required to confirm their identity via the password reset functionality.", self::ID) . "\n";
|
1507 |
+
|
1508 |
+
return wp_mail($to, $subject, $message);
|
1509 |
+
}
|
1510 |
+
|
1511 |
+
/**
|
1512 |
+
* Sends an email to the blog's administrator telling them that the site
|
1513 |
+
* is being attacked
|
1514 |
+
*
|
1515 |
+
* @param string $network_ip a prior result from get_network_ip()
|
1516 |
+
* @param string $user_name the user name from the current login form
|
1517 |
+
* @param string $pass_md5 the md5 hashed new password
|
1518 |
+
* @return bool
|
1519 |
+
*
|
1520 |
+
* @uses login_security_solution::get_notify_counts() for some shared text
|
1521 |
+
* @uses wp_mail() to send the messages
|
1522 |
+
*/
|
1523 |
+
protected function notify_fail($network_ip, $user_name, $pass_md5,
|
1524 |
+
$fails)
|
1525 |
+
{
|
1526 |
+
$this->load_plugin_textdomain();
|
1527 |
+
|
1528 |
+
$to = $this->sanitize_whitespace(get_option('admin_email'));
|
1529 |
+
|
1530 |
+
$blog = get_option('blogname');
|
1531 |
+
$subject = sprintf('ATTACK HAPPENING TO %s', $blog);
|
1532 |
+
$subject = $this->sanitize_whitespace($subject);
|
1533 |
+
|
1534 |
+
$message =
|
1535 |
+
sprintf(__("Your website, %s, is undergoing a brute force attack.", self::ID),
|
1536 |
+
$blog) . "\n\n"
|
1537 |
+
|
1538 |
+
. sprintf(__("There have been at least %d failed attempts to log in during the past %d minutes that used one or more of the following components:", self::ID),
|
1539 |
+
$fails['total'], $this->options['login_fail_minutes']) . "\n\n"
|
1540 |
+
|
1541 |
+
. $this->get_notify_counts($network_ip, $user_name, $pass_md5, $fails)
|
1542 |
+
|
1543 |
+
. sprintf(__("The %s plugin for WordPress is repelling the attack by making their login failures take a very long time.", self::ID),
|
1544 |
+
self::NAME) . "\n";
|
1545 |
+
|
1546 |
+
return wp_mail($to, $subject, $message);
|
1547 |
+
}
|
1548 |
+
|
1549 |
+
/**
|
1550 |
+
* Records the failed login, disconnects the database, then calls sleep()
|
1551 |
+
* for increasing amounts of time as more failures come in
|
1552 |
+
*
|
1553 |
+
* @param string $user_name the user name from the current login form
|
1554 |
+
* @param string $user_pass the unhashed new password
|
1555 |
+
* @return int the number of seconds sleep()'ed (for use by unit tests)
|
1556 |
+
*
|
1557 |
+
* @uses login_security_solution::get_ip() to get the IP address
|
1558 |
+
* @uses login_security_solution::get_network_ip() gets the IP's
|
1559 |
+
* "network" part
|
1560 |
+
* @uses login_security_solution::md5() to hash the password
|
1561 |
+
* @uses login_security_solution::get_login_fail() to see if
|
1562 |
+
* they're over the limit
|
1563 |
+
* @uses login_security_solution::notify_fail() to warn of an attack
|
1564 |
+
*/
|
1565 |
+
protected function process_login_fail($user_name, $user_pass) {
|
1566 |
+
global $wpdb;
|
1567 |
+
|
1568 |
+
$ip = $this->get_ip();
|
1569 |
+
$network_ip = $this->get_network_ip($ip);
|
1570 |
+
$pass_md5 = $this->md5($user_pass);
|
1571 |
+
|
1572 |
+
$this->insert_fail($ip, $user_name, $pass_md5);
|
1573 |
+
|
1574 |
+
$fails = $this->get_login_fail($network_ip, $user_name, $pass_md5);
|
1575 |
+
|
1576 |
+
if ($this->options['login_fail_notify']
|
1577 |
+
&& ! ($fails['total'] % $this->options['login_fail_notify']))
|
1578 |
+
{
|
1579 |
+
$this->notify_fail($network_ip, $user_name, $pass_md5, $fails);
|
1580 |
+
}
|
1581 |
+
|
1582 |
+
$sleep = 0;
|
1583 |
+
if ($fails['total'] < $this->options['login_fail_tier_2']) {
|
1584 |
+
// Use random, overlapping sleep times to complicate profiling.
|
1585 |
+
$sleep = rand(1, 7);
|
1586 |
+
} elseif ($fails['total'] < $this->options['login_fail_tier_3']) {
|
1587 |
+
$sleep = rand(4, 30);
|
1588 |
+
} else {
|
1589 |
+
$sleep = rand(25, 60);
|
1590 |
+
}
|
1591 |
+
|
1592 |
+
if ($sleep) {
|
1593 |
+
if (!$this->testing) {
|
1594 |
+
// Keep login failures from becoming denial of service attacks.
|
1595 |
+
mysql_close($wpdb->dbh);
|
1596 |
+
|
1597 |
+
// Increasingly slow down attackers to the point they'll give up.
|
1598 |
+
sleep($sleep);
|
1599 |
+
}
|
1600 |
+
}
|
1601 |
+
|
1602 |
+
return $sleep;
|
1603 |
+
}
|
1604 |
+
|
1605 |
+
/**
|
1606 |
+
* Updates and removes the password related user metadata as needed
|
1607 |
+
*
|
1608 |
+
* For use when a password is changed.
|
1609 |
+
*
|
1610 |
+
* @param int $user_ID the user's id number
|
1611 |
+
* @param string $user_pass the unhashed new password
|
1612 |
+
* @return void
|
1613 |
+
*/
|
1614 |
+
protected function process_pw_metadata($user_ID, $user_pass) {
|
1615 |
+
if ($this->options['pw_change_days']) {
|
1616 |
+
$this->set_pw_changed_time($user_ID);
|
1617 |
+
}
|
1618 |
+
if ($this->options['pw_reuse_count']) {
|
1619 |
+
$this->save_pw_hash($user_ID, wp_hash_password($user_pass));
|
1620 |
+
}
|
1621 |
+
$this->delete_pw_force_change($user_ID);
|
1622 |
+
$this->delete_pw_grace_period($user_ID);
|
1623 |
+
}
|
1624 |
+
|
1625 |
+
/**
|
1626 |
+
* Sends HTTP Location headers that direct users to the login page
|
1627 |
+
*
|
1628 |
+
* Also permits adding message ID's to the URI query string that get
|
1629 |
+
* interpreted by our login_message() method, which displays them above
|
1630 |
+
* the login form.
|
1631 |
+
*
|
1632 |
+
* Utilizes WordPress' "redirect_to" functionality to bring users back to
|
1633 |
+
* where they came from once they have logged in.
|
1634 |
+
*
|
1635 |
+
* @param string $login_msg_id the ID representing the message to
|
1636 |
+
* display above the login form
|
1637 |
+
* @param bool $use_rt use WP's "redirect_to" on successful login?
|
1638 |
+
* @param bool $action "login" (default) or "retrievepassword"
|
1639 |
+
* @return void
|
1640 |
+
*
|
1641 |
+
* @uses login_security_solution::$key_login_msg to know which $_GET
|
1642 |
+
* parameter to put the message id into
|
1643 |
+
* @see login_security_solution::login_message() for rendering the
|
1644 |
+
* messages
|
1645 |
+
* @uses wp_login_url() to know where the login form is
|
1646 |
+
* @uses wp_logout() to deactivate the current session
|
1647 |
+
* @uses wp_redirect() to perform the actual redirect
|
1648 |
+
*/
|
1649 |
+
protected function redirect_to_login($login_msg_id = '', $use_rt = false,
|
1650 |
+
$action = 'login')
|
1651 |
+
{
|
1652 |
+
if ($use_rt && !empty($_SERVER['REQUEST_URI'])) {
|
1653 |
+
$uri = wp_login_url($_SERVER['REQUEST_URI']);
|
1654 |
+
} else {
|
1655 |
+
$uri = wp_login_url();
|
1656 |
+
}
|
1657 |
+
$uri = $this->sanitize_whitespace($uri);
|
1658 |
+
|
1659 |
+
if (strpos($uri, '?') === false) {
|
1660 |
+
$uri .= '?';
|
1661 |
+
} else {
|
1662 |
+
$uri .= '&';
|
1663 |
+
}
|
1664 |
+
$uri .= 'action=' . urlencode($action);
|
1665 |
+
|
1666 |
+
if ($login_msg_id) {
|
1667 |
+
$uri .= '&' . urlencode($this->key_login_msg) . '='
|
1668 |
+
. urlencode($login_msg_id);
|
1669 |
+
}
|
1670 |
+
|
1671 |
+
wp_logout();
|
1672 |
+
wp_redirect($uri);
|
1673 |
+
|
1674 |
+
if (!$this->testing) {
|
1675 |
+
exit;
|
1676 |
+
}
|
1677 |
+
}
|
1678 |
+
|
1679 |
+
/**
|
1680 |
+
* Replaces all whitespace characters with one space
|
1681 |
+
* @param string $in the string to clean
|
1682 |
+
* @return string the cleaned string
|
1683 |
+
*/
|
1684 |
+
protected function sanitize_whitespace($in) {
|
1685 |
+
return preg_replace('/\s+/', ' ', $in);
|
1686 |
+
}
|
1687 |
+
|
1688 |
+
/**
|
1689 |
+
* Logs password hashes to prevent passwords from being reused frequently
|
1690 |
+
*
|
1691 |
+
* Note: duplicate hashes are not stored.
|
1692 |
+
*
|
1693 |
+
* @param int $user_ID the user's id number
|
1694 |
+
* @param string $new_hash the wp hashed password to save
|
1695 |
+
* @return mixed true on success, 1 if hash is already stored
|
1696 |
+
*/
|
1697 |
+
protected function save_pw_hash($user_ID, $new_hash) {
|
1698 |
+
$hashes = $this->get_pw_hashes($user_ID);
|
1699 |
+
|
1700 |
+
if (in_array($new_hash, $hashes)) {
|
1701 |
+
return 1;
|
1702 |
+
}
|
1703 |
+
|
1704 |
+
$hashes[] = $new_hash;
|
1705 |
+
|
1706 |
+
$cut = count($hashes) - $this->options['pw_reuse_count'];
|
1707 |
+
if ($cut > 0) {
|
1708 |
+
array_splice($hashes, 0, $cut);
|
1709 |
+
}
|
1710 |
+
|
1711 |
+
update_user_meta($user_ID, $this->umk_hashes, $hashes);
|
1712 |
+
|
1713 |
+
return true;
|
1714 |
+
}
|
1715 |
+
|
1716 |
+
/**
|
1717 |
+
* Stores the present time in the given user's "last active" metadata
|
1718 |
+
*
|
1719 |
+
* @param int $user_ID the current user's ID number
|
1720 |
+
* @return int|bool the record number if added, TRUE if updated, FALSE
|
1721 |
+
* if error
|
1722 |
+
*/
|
1723 |
+
protected function set_last_active($user_ID) {
|
1724 |
+
return update_user_meta($user_ID, $this->umk_last_active, time());
|
1725 |
+
}
|
1726 |
+
|
1727 |
+
/**
|
1728 |
+
* Replaces the default option values with those stored in the database
|
1729 |
+
* @uses login_security_solution::$options to hold the data
|
1730 |
+
*/
|
1731 |
+
protected function set_options() {
|
1732 |
+
$options = get_option($this->option_name);
|
1733 |
+
if (!is_array($options)) {
|
1734 |
+
$options = array();
|
1735 |
+
}
|
1736 |
+
$this->options = array_merge($this->options_default, $options);
|
1737 |
+
}
|
1738 |
+
|
1739 |
+
/**
|
1740 |
+
* Stores the present time in the given user's "password changed" metadata
|
1741 |
+
*
|
1742 |
+
* @param int $user_ID the current user's ID number
|
1743 |
+
* @return int|bool the record number if added, TRUE if updated, FALSE
|
1744 |
+
* if error
|
1745 |
+
*/
|
1746 |
+
protected function set_pw_changed_time($user_ID) {
|
1747 |
+
return update_user_meta($user_ID, $this->umk_changed, time());
|
1748 |
+
}
|
1749 |
+
|
1750 |
+
/**
|
1751 |
+
* Puts the "force password change" flag into the user's metadata
|
1752 |
+
*
|
1753 |
+
* @param int $user_ID the current user's ID number
|
1754 |
+
* @return int|bool the record number if added, TRUE if updated, FALSE
|
1755 |
+
* if error
|
1756 |
+
*/
|
1757 |
+
protected function set_pw_force_change($user_ID) {
|
1758 |
+
return update_user_meta($user_ID, $this->umk_pw_force_change, 1);
|
1759 |
+
}
|
1760 |
+
|
1761 |
+
/**
|
1762 |
+
* Stores the present time in the given user's "password grace period"
|
1763 |
+
* metadata
|
1764 |
+
*
|
1765 |
+
* @param int $user_ID the current user's ID number
|
1766 |
+
* @return int|bool the record number if added, TRUE if updated, FALSE
|
1767 |
+
* if error
|
1768 |
+
*/
|
1769 |
+
protected function set_pw_grace_period($user_ID) {
|
1770 |
+
return update_user_meta($user_ID, $this->umk_grace_period, time());
|
1771 |
+
}
|
1772 |
+
|
1773 |
+
/**
|
1774 |
+
* Breaks a password up into an array of individual characters
|
1775 |
+
*
|
1776 |
+
* @param string $pw the password to examine
|
1777 |
+
* @return array
|
1778 |
+
*/
|
1779 |
+
protected function split($pw) {
|
1780 |
+
return preg_split('/(?<!^)(?!$)/u', $pw);
|
1781 |
+
}
|
1782 |
+
|
1783 |
+
/**
|
1784 |
+
* Determines how long a string is using mb_strlen() if available
|
1785 |
+
*
|
1786 |
+
* @param string $pw the string to evaluate
|
1787 |
+
* @return int the length of the string
|
1788 |
+
*/
|
1789 |
+
protected function strlen($pw) {
|
1790 |
+
if ($this->available_mbstring) {
|
1791 |
+
return mb_strlen($pw);
|
1792 |
+
} else {
|
1793 |
+
return strlen($pw);
|
1794 |
+
}
|
1795 |
+
}
|
1796 |
+
|
1797 |
+
/**
|
1798 |
+
* Removes non-letter and non-numeric characters from the password
|
1799 |
+
*
|
1800 |
+
* @param string $pw the password to examine
|
1801 |
+
* @return string
|
1802 |
+
*/
|
1803 |
+
protected function strip_nonword_chars($pw) {
|
1804 |
+
return preg_replace('/[^\p{L}\p{Nd}]/u', '', $pw);
|
1805 |
+
}
|
1806 |
+
|
1807 |
+
/**
|
1808 |
+
* Reverses a string in a multibyte safe way
|
1809 |
+
*
|
1810 |
+
* @param sring $pw the string to examine
|
1811 |
+
* @return sring the reversed string
|
1812 |
+
*/
|
1813 |
+
protected function strrev($pw) {
|
1814 |
+
return implode('', array_reverse($this->split($pw)));
|
1815 |
+
}
|
1816 |
+
|
1817 |
+
/**
|
1818 |
+
* Extracts parts of strings, using mb_substr() if available
|
1819 |
+
*
|
1820 |
+
* @param string $pw the string to evaluate
|
1821 |
+
* @param int $start the starting index (0 based)
|
1822 |
+
* @param int $length the number of characters to get
|
1823 |
+
* @return string the desired part of the password
|
1824 |
+
*/
|
1825 |
+
protected function substr($pw, $start, $length) {
|
1826 |
+
if ($this->available_mbstring) {
|
1827 |
+
return mb_substr($pw, $start, $length);
|
1828 |
+
} else {
|
1829 |
+
return substr($pw, $start, $length);
|
1830 |
+
}
|
1831 |
+
}
|
1832 |
+
|
1833 |
+
/**
|
1834 |
+
* Is the password valid?
|
1835 |
+
*
|
1836 |
+
* @param WP_User|string the user object or password to be examined
|
1837 |
+
* @param WP_Error the means to provide specific error messages
|
1838 |
+
* @return bool
|
1839 |
+
*/
|
1840 |
+
public function validate_pw($user, &$errors = null) {
|
1841 |
+
$this->load_plugin_textdomain();
|
1842 |
+
|
1843 |
+
if (is_object($user)) {
|
1844 |
+
$all_tests = true;
|
1845 |
+
|
1846 |
+
if (empty($user->user_pass)) {
|
1847 |
+
if ($errors !== null) {
|
1848 |
+
$errors->add(self::ID,
|
1849 |
+
__("<strong>ERROR</strong>: Password not set.", self::ID),
|
1850 |
+
array('form-field' => 'pass1')
|
1851 |
+
);
|
1852 |
+
}
|
1853 |
+
return false;
|
1854 |
+
}
|
1855 |
+
$pw = $user->user_pass;
|
1856 |
+
} else {
|
1857 |
+
$all_tests = false;
|
1858 |
+
$pw = $user;
|
1859 |
+
}
|
1860 |
+
|
1861 |
+
if (!is_string($pw)) {
|
1862 |
+
if ($errors !== null) {
|
1863 |
+
$errors->add(self::ID,
|
1864 |
+
__("<strong>ERROR</strong>: Passwords must be strings.", self::ID),
|
1865 |
+
array('form-field' => 'pass1')
|
1866 |
+
);
|
1867 |
+
}
|
1868 |
+
return false;
|
1869 |
+
}
|
1870 |
+
|
1871 |
+
$pw = trim($pw);
|
1872 |
+
|
1873 |
+
if ($this->available_mbstring === null) {
|
1874 |
+
$this->available_mbstring = extension_loaded('mbstring');
|
1875 |
+
}
|
1876 |
+
|
1877 |
+
if (!$this->available_mbstring
|
1878 |
+
&& $this->is_pw_outside_ascii($pw))
|
1879 |
+
{
|
1880 |
+
if ($errors !== null) {
|
1881 |
+
$errors->add(self::ID,
|
1882 |
+
__("<strong>ERROR</strong>: Passwords must use ASCII characters.", self::ID),
|
1883 |
+
array('form-field' => 'pass1')
|
1884 |
+
);
|
1885 |
+
}
|
1886 |
+
return false;
|
1887 |
+
}
|
1888 |
+
|
1889 |
+
$length = $this->strlen($pw);
|
1890 |
+
if ($length < $this->options['pw_complexity_exemption_length']) {
|
1891 |
+
$enforce_complexity = true;
|
1892 |
+
} else {
|
1893 |
+
$enforce_complexity = false;
|
1894 |
+
}
|
1895 |
+
|
1896 |
+
// NOTE: tests ordered from fastest to slowest.
|
1897 |
+
|
1898 |
+
if ($length < $this->options['pw_length']) {
|
1899 |
+
if ($errors !== null) {
|
1900 |
+
$errors->add(self::ID,
|
1901 |
+
__("<strong>ERROR</strong>: Password is too short.", self::ID),
|
1902 |
+
array('form-field' => 'pass1')
|
1903 |
+
);
|
1904 |
+
}
|
1905 |
+
return false;
|
1906 |
+
}
|
1907 |
+
if ($enforce_complexity && $this->is_pw_missing_numeric($pw)) {
|
1908 |
+
if ($errors !== null) {
|
1909 |
+
$errors->add(self::ID,
|
1910 |
+
sprintf(__("<strong>ERROR</strong>: Passwords must either contain numbers or be %d characters long.", self::ID), $this->options['pw_complexity_exemption_length']),
|
1911 |
+
array('form-field' => 'pass1')
|
1912 |
+
);
|
1913 |
+
}
|
1914 |
+
return false;
|
1915 |
+
}
|
1916 |
+
if ($enforce_complexity && $this->is_pw_missing_punct_chars($pw)) {
|
1917 |
+
if ($errors !== null) {
|
1918 |
+
$errors->add(self::ID,
|
1919 |
+
sprintf(__("<strong>ERROR</strong>: Passwords must either contain punctuation marks / symbols or be %d characters long.", self::ID), $this->options['pw_complexity_exemption_length']),
|
1920 |
+
array('form-field' => 'pass1')
|
1921 |
+
);
|
1922 |
+
}
|
1923 |
+
return false;
|
1924 |
+
}
|
1925 |
+
if ($enforce_complexity && $this->is_pw_missing_upper_lower_chars($pw)) {
|
1926 |
+
if ($errors !== null) {
|
1927 |
+
$errors->add(self::ID,
|
1928 |
+
sprintf(__("<strong>ERROR</strong>: Passwords must either contain upper-case and lower-case letters or be %d characters long.", self::ID), $this->options['pw_complexity_exemption_length']),
|
1929 |
+
array('form-field' => 'pass1')
|
1930 |
+
);
|
1931 |
+
}
|
1932 |
+
return false;
|
1933 |
+
}
|
1934 |
+
|
1935 |
+
if ($this->is_pw_sequential_file($pw)) {
|
1936 |
+
if ($errors !== null) {
|
1937 |
+
$errors->add(self::ID,
|
1938 |
+
__("<strong>ERROR</strong>: Passwords can't be sequential keys.", self::ID),
|
1939 |
+
array('form-field' => 'pass1')
|
1940 |
+
);
|
1941 |
+
}
|
1942 |
+
return false;
|
1943 |
+
}
|
1944 |
+
if ($this->is_pw_sequential_codepoints($pw)) {
|
1945 |
+
if ($errors !== null) {
|
1946 |
+
$errors->add(self::ID,
|
1947 |
+
__("<strong>ERROR</strong>: Passwords can't have that many sequential characters.", self::ID),
|
1948 |
+
array('form-field' => 'pass1')
|
1949 |
+
);
|
1950 |
+
}
|
1951 |
+
return false;
|
1952 |
+
}
|
1953 |
+
|
1954 |
+
$non_leet = $this->convert_leet_speak($pw);
|
1955 |
+
$stripped = $this->strip_nonword_chars($non_leet);
|
1956 |
+
|
1957 |
+
if ($all_tests
|
1958 |
+
&& ($this->is_pw_like_user_data($pw, $user)
|
1959 |
+
|| $this->is_pw_like_user_data($stripped, $user)))
|
1960 |
+
{
|
1961 |
+
if ($errors !== null) {
|
1962 |
+
$errors->add(self::ID,
|
1963 |
+
__("<strong>ERROR</strong>: Passwords can't contain user data.", self::ID),
|
1964 |
+
array('form-field' => 'pass1')
|
1965 |
+
);
|
1966 |
+
}
|
1967 |
+
return false;
|
1968 |
+
}
|
1969 |
+
if ($this->is_pw_like_bloginfo($pw)
|
1970 |
+
|| $this->is_pw_like_bloginfo($stripped))
|
1971 |
+
{
|
1972 |
+
if ($errors !== null) {
|
1973 |
+
$errors->add(self::ID,
|
1974 |
+
__("<strong>ERROR</strong>: Passwords can't contain site info.", self::ID),
|
1975 |
+
array('form-field' => 'pass1')
|
1976 |
+
);
|
1977 |
+
}
|
1978 |
+
return false;
|
1979 |
+
}
|
1980 |
+
if ($all_tests && $this->is_pw_dictionary($pw)) {
|
1981 |
+
if ($errors !== null) {
|
1982 |
+
$errors->add(self::ID,
|
1983 |
+
__("<strong>ERROR</strong>: Password is too common.", self::ID),
|
1984 |
+
array('form-field' => 'pass1')
|
1985 |
+
);
|
1986 |
+
}
|
1987 |
+
return false;
|
1988 |
+
}
|
1989 |
+
if ($this->is_pw_dict_program($stripped)) {
|
1990 |
+
if ($errors !== null) {
|
1991 |
+
$errors->add(self::ID,
|
1992 |
+
__("<strong>ERROR</strong>: Passwords can't be variations of dictionary words.", self::ID),
|
1993 |
+
array('form-field' => 'pass1')
|
1994 |
+
);
|
1995 |
+
}
|
1996 |
+
return false;
|
1997 |
+
}
|
1998 |
+
|
1999 |
+
return true;
|
2000 |
+
}
|
2001 |
+
}
|
pw_dictionaries/test.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
1 |
+
This1sAnObscure)asswordForTesting
|
2 |
+
Pa$$w0rd
|
3 |
+
Pa$$w0rd1
|
4 |
+
简化字的昨天今天和明天
|
pw_sequences/us-101-keyboard.txt
ADDED
@@ -0,0 +1 @@
|
|
|
1 |
+
~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?`1234567890-=qwertyuiop[]\asdfghjkl;'zxcvbnm,./
|
readme.txt
ADDED
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
=== Login Security Solution ===
|
2 |
+
Contributors: convissor
|
3 |
+
Tags: login, password, idle, timeout, maintenance, security, attack, hack, lock, ban
|
4 |
+
Requires at least: 3.0
|
5 |
+
Tested up to: 3.3.1
|
6 |
+
Stable tag: trunk
|
7 |
+
|
8 |
+
Repels brute force attacks (by IP, name, password). Requires very strong passwords (complex, don't match site/user info, etc). Plus much more!
|
9 |
+
|
10 |
+
== Description ==
|
11 |
+
|
12 |
+
* Blocks brute force and dictionary attacks without inconveniencing
|
13 |
+
legitimate users
|
14 |
+
+ Tracks IP addresses, usernames, and passwords
|
15 |
+
+ If a login failure uses data matching a past failure, the plugin
|
16 |
+
slows down response times. The more failures, the longer the delay.
|
17 |
+
This encourages attackers to give up and go find an easier target.
|
18 |
+
+ If an account seems breached, the "user" is immediately logged out
|
19 |
+
and forced to use WordPress' password reset utility. This prevents
|
20 |
+
any damage from being done and verifies the user's identity. All
|
21 |
+
without intervention by an administrator.
|
22 |
+
+ Can notify the administrator of attacks and breaches
|
23 |
+
+ Supports IPv6
|
24 |
+
|
25 |
+
* Thoroughly examines the strength of new passwords. Includes full
|
26 |
+
UTF-8 character set support if PHP's `mbstring` extension is enabled.
|
27 |
+
The tests have caught every password dictionary entry I've tried.
|
28 |
+
+ Minimum length (customizable)
|
29 |
+
+ Doesn't match blog info
|
30 |
+
+ Doesn't match user data
|
31 |
+
+ Must either have numbers in it or be very long
|
32 |
+
+ Must either have punctuation, upper and lower case characters or be
|
33 |
+
very long
|
34 |
+
+ Non-sequential codepoints
|
35 |
+
+ Non-sequential keystrokes (custom sequence files can be added)
|
36 |
+
+ Not in the password dictionary files you've provided (if any)
|
37 |
+
+ Decodes "leet" speak
|
38 |
+
+ Not found by the `dict` dictionary program (if available)
|
39 |
+
|
40 |
+
* Password aging (optional)
|
41 |
+
+ Users need to change password every x days (customizable)
|
42 |
+
+ Grace period for picking a new password (customizable)
|
43 |
+
+ Remembers old passwords (quantity is customizable)
|
44 |
+
|
45 |
+
* Administrators can require all users to change their passwords
|
46 |
+
+ Done via a flag in each user's database entry
|
47 |
+
+ No mail is sent, keeping your server off of spam lists
|
48 |
+
|
49 |
+
* Logs out idle sessions (optional) (idle time is customizable)
|
50 |
+
|
51 |
+
* Maintenance mode (optional)
|
52 |
+
+ Publicly viewable content remains visible
|
53 |
+
+ Disables logins by all users, except administrators
|
54 |
+
+ Logs out existing sessions, except administrators
|
55 |
+
+ Disables posting of comments
|
56 |
+
+ Useful for maintenance or emergency reasons
|
57 |
+
+ This is separate from WordPress' maintenance mode
|
58 |
+
|
59 |
+
* Prevents information disclosures from failed logins
|
60 |
+
|
61 |
+
|
62 |
+
= Improvements Over Similar WordPress Plugins =
|
63 |
+
|
64 |
+
* The plugin itself is secure against SQL, HTML, and header injections
|
65 |
+
* Notice-free code means no information disclosures if error_reporting = E_ALL
|
66 |
+
* Only loads files, actions, and filters needed for enabled options
|
67 |
+
and the page's context
|
68 |
+
* Provides an option to have deactivation remove all of this plugin's
|
69 |
+
data from the database
|
70 |
+
* Uses WordPress' features rather than fighting or overriding them
|
71 |
+
* No advertising, promotions, or beacons
|
72 |
+
* Proper internationalization support
|
73 |
+
* Clean, documented code
|
74 |
+
* Unit tests covering 100% of the main class
|
75 |
+
|
76 |
+
|
77 |
+
= Securing Your WordPress Site is Important =
|
78 |
+
|
79 |
+
You're probably thinking "There's nothing valuable on my website. No one
|
80 |
+
will bother breaking into it." What you need to realize is that attackers
|
81 |
+
are going after your visitors. They put stealth code on your website
|
82 |
+
that pushes malware into the browsers of the people looking at your site.
|
83 |
+
|
84 |
+
> According to SophosLabs more than 30,000 websites are infected
|
85 |
+
> every day and 80% of those infected sites are legitimate.
|
86 |
+
> Eighty-five percent of all malware, including viruses, worms,
|
87 |
+
> spyware, adware and Trojans, comes from the web. Today,
|
88 |
+
> drive-by downloads have become the top web threat.
|
89 |
+
>
|
90 |
+
> -- [*Security Threat Report 2012*](http://www.sophos.com/en-us/security-news-trends/reports/security-threat-report/html-08.aspx)
|
91 |
+
|
92 |
+
So if your site does get cracked, not only do you waste hours cleaning up,
|
93 |
+
your reputation is sullied, security software flags your site as dangerous,
|
94 |
+
and worst of all, you've inadvertently helped infect the computers of your
|
95 |
+
clients and friends.
|
96 |
+
|
97 |
+
|
98 |
+
== Installation ==
|
99 |
+
|
100 |
+
1. Download the package
|
101 |
+
from `http://wordpress.org/extend/plugins/login-security-solution/`
|
102 |
+
|
103 |
+
1. Unzip the file.
|
104 |
+
|
105 |
+
1. Our existing tests are very effective, catching all of the 2 million
|
106 |
+
entries in the Dazzlepod password list. But if you need to block
|
107 |
+
specific passwords that my tests miss, this plugin offers the ability
|
108 |
+
to provide your own dictionary files.
|
109 |
+
|
110 |
+
Add a file to the `pw_dictionaries` directory and place those passwords
|
111 |
+
in it. One password per line.
|
112 |
+
|
113 |
+
Please be aware that checking the password files is computationally
|
114 |
+
expensive. The following script runs through each of the password
|
115 |
+
files and weeds out passwords caught by the other
|
116 |
+
tests:
|
117 |
+
|
118 |
+
php utilities/reduce-dictionary-files.php
|
119 |
+
|
120 |
+
1. If your website has a large number of non-English-speaking users:
|
121 |
+
|
122 |
+
* See if a keyboard sequence file exists in this plugin's
|
123 |
+
`pw_sequences` directory for your target languages. The following steps
|
124 |
+
are for left-to-right languages. (For right-to-left languages, flip the
|
125 |
+
direction of the motions indicated.)
|
126 |
+
+ Open a text editor and create a file in the `pw_sequences`
|
127 |
+
directory
|
128 |
+
+ Hold down the shift key
|
129 |
+
+ Press the top left **character** key of the keyboard.
|
130 |
+
NOTE: during this entire process, do not press function, control
|
131 |
+
or whitespace keys (like tab, enter, delete, arrows, space, etc).
|
132 |
+
+ Work your way across the top row, pressing each key across the
|
133 |
+
row, one by one
|
134 |
+
+ Press the left-most character key in the second row
|
135 |
+
+ Go across the second row pressing each key
|
136 |
+
+ Continue through the entire keyboard in the same manner
|
137 |
+
+ Let go of the shift key
|
138 |
+
+ Re-start the process at the top left key of the keyboard and
|
139 |
+
work your way through the keyboard, now in lower-case mode
|
140 |
+
+ Save the file and close the editor
|
141 |
+
+ Feel free to submit the files to me so others can use it. See
|
142 |
+
the features request section, below.
|
143 |
+
|
144 |
+
* If a translation file for your language does not exist in this
|
145 |
+
plugin's `languages` directory, add one. Read
|
146 |
+
http://codex.wordpress.org/I18n_for_WordPress_Developers
|
147 |
+
for details. Send me the file and I'll include it in future
|
148 |
+
releases. See the features request section, below.
|
149 |
+
|
150 |
+
1. The last step of the new password validation process is checking if
|
151 |
+
the password matches an entry in the `dict` program. See if `dict`
|
152 |
+
is installed on your server and consider installing it if not.
|
153 |
+
http://en.wikipedia.org/wiki/Dict
|
154 |
+
|
155 |
+
1. Upload the `login-security-solution` directory to your
|
156 |
+
server's `/wp-content/plugins/` directory
|
157 |
+
|
158 |
+
1. Activate the plugin through the "Plugins" menu in WordPress.
|
159 |
+
|
160 |
+
1. Adjust the settings as desired. This plugin's settings page can be
|
161 |
+
reached via a sub-menu entry under WordPress' "Settings" menu or this
|
162 |
+
plugin's entry on WordPress' "Plugins" page.
|
163 |
+
|
164 |
+
1. Run the "Change All Passwords" process. This is necessary to ensure
|
165 |
+
all of your users have strong passwords. The user interface for
|
166 |
+
doing so is accessible via a link in this plugin's entry on
|
167 |
+
WordPress' "Plugins" page.
|
168 |
+
|
169 |
+
1. Ensure your password is strong by changing it.
|
170 |
+
|
171 |
+
|
172 |
+
= Unit Tests =
|
173 |
+
|
174 |
+
A thorough set of unit tests are found in the `tests` directory.
|
175 |
+
|
176 |
+
The plugin needs to be placed in the `wp-contents/plugins` directory of
|
177 |
+
a working WordPress installation. The plugin does not need to be
|
178 |
+
activated for the tests to run.
|
179 |
+
|
180 |
+
To execute the tests, `cd` into the `tests` directory and call `phpunit .`.
|
181 |
+
|
182 |
+
Please note that the tests make extensive use of database transactions.
|
183 |
+
Many tests will be skipped if your `wp_options` and `wp_usermeta` tables
|
184 |
+
are not using the `InnoDB` storage engine.
|
185 |
+
|
186 |
+
|
187 |
+
= Removal =
|
188 |
+
|
189 |
+
1. This plugin offers the ability to remove all of this plugin's settings
|
190 |
+
from your database. Go to WordPress' "Plugins" admin interface and
|
191 |
+
click the "Settings" link for this plugin. In the "Deactivate" entry,
|
192 |
+
click the "Yes, delete the damn data" button and save the form.
|
193 |
+
|
194 |
+
1. Use WordPress' "Plugins" admin interface to and click the "Deactivate"
|
195 |
+
link.
|
196 |
+
|
197 |
+
1. Remove the `login-security-solution` directory.
|
198 |
+
|
199 |
+
|
200 |
+
== Frequently Asked Questions ==
|
201 |
+
|
202 |
+
Ask and ye shall receive.
|
203 |
+
|
204 |
+
|
205 |
+
== Changelog ==
|
206 |
+
|
207 |
+
= 0.0.4 =
|
208 |
+
* Initial import to `plugins.svn.wordpress.org`.
|
209 |
+
|
210 |
+
= 0.0.3 =
|
211 |
+
* Fix mixups in the code saving the "Change All Passwords" admin UI.
|
212 |
+
* Adjust IdleTest so it doesn't radically change `wp_users` auto increment.
|
213 |
+
* Tested under WordPress 3.3.1.
|
214 |
+
* Unit tests pass using PHP 5.4.0RC8-dev, 5.3.11-dev, and 5.2.18-dev.
|
215 |
+
|
216 |
+
= 0.0.2 =
|
217 |
+
* Use Unicode character properties to improve portability.
|
218 |
+
* Stop tests short if not in a wp install.
|
219 |
+
* Skip dict test if dict not available.
|
220 |
+
* Skip database tests if transactions are not available.
|
221 |
+
* Tested under WordPress 3.3.1.
|
222 |
+
* Unit tests pass using PHP 5.4.0RC8-dev, 5.3.11-dev, and 5.2.18-dev.
|
223 |
+
|
224 |
+
= 0.0.1 =
|
225 |
+
* Post the code for public review.
|
226 |
+
* Tested under WordPress 3.3.1.
|
227 |
+
|
228 |
+
|
229 |
+
== To Do ==
|
230 |
+
|
231 |
+
* Delete old data in the `fail` table.
|
232 |
+
* Add some JS/AJAX magic to make users' lives easier by also validating
|
233 |
+
passwords on the front end prior to submission. Patches welcome!
|
234 |
+
|
235 |
+
|
236 |
+
== Bugs and Feature Requests ==
|
237 |
+
|
238 |
+
Report bugs and submit feature requests by opening a ticket in WordPress'
|
239 |
+
plugins Trac website: http://plugins.trac.wordpress.org/report.
|
240 |
+
Select `login-security-solution` in the "Component" list.
|
241 |
+
|
242 |
+
|
243 |
+
== Inspiration and References ==
|
244 |
+
|
245 |
+
* Password Research
|
246 |
+
+ [You can never have too many passwords: techniques for evaluating a huge corpus](http://www.cl.cam.ac.uk/~jcb82/doc/B12-IEEESP-evaluating_a_huge_password_corpus.pdf)
|
247 |
+
+ [Analyzing Password Strength](http://www.cs.ru.nl/bachelorscripties/2010/Martin_Devillers___0437999___Analyzing_password_strength.pdf)
|
248 |
+
+ [Consumer Password Worst Practices](http://www.imperva.com/docs/WP_Consumer_Password_Worst_Practices.pdf)
|
249 |
+
+ [Preventing Brute Force Attacks on your Web Login](http://www.bryanrite.com/preventing-brute-force-attacks-on-your-web-login/)
|
250 |
+
+ [Password Strength](http://xkcd.com/936/)
|
251 |
+
|
252 |
+
* Technical Info
|
253 |
+
+ [The Extreme UTF-8 Table](http://doc.infosnel.nl/extreme_utf-8.html)
|
254 |
+
+ [A Recommendation for IPv6 Address Text Representation](http://tools.ietf.org/html/rfc5952)
|
255 |
+
|
256 |
+
* Password Lists
|
257 |
+
+ [Dazzlepod Password List](http://dazzlepod.com/site_media/txt/passwords.txt)
|
258 |
+
+ [Common Passwords](http://www.searchlores.org/commonpass1.htm)
|
259 |
+
+ [The Top 500 Worst Passwords of All Time](http://www.whatsmypass.com/the-top-500-worst-passwords-of-all-time)
|
tests/Accessor.php
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Extend the class to be tested, providing access to protected elements
|
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 |
+
* Obtain the parent class.
|
14 |
+
* Use dirname(dirname()) because safe mode can disable "../".
|
15 |
+
*/
|
16 |
+
require_once dirname(dirname(__FILE__)) . '/login-security-solution.php';
|
17 |
+
|
18 |
+
/**
|
19 |
+
* Get the admin class
|
20 |
+
*/
|
21 |
+
require_once dirname(dirname(__FILE__)) . '/admin.inc';
|
22 |
+
|
23 |
+
// Remove automatically created object.
|
24 |
+
unset($GLOBALS['login_security_solution']);
|
25 |
+
|
26 |
+
/**
|
27 |
+
* Extend the class to be tested, providing access to protected elements
|
28 |
+
*
|
29 |
+
* @package login-security-solution
|
30 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
31 |
+
* @copyright The Analysis and Solutions Company, 2012
|
32 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
33 |
+
*/
|
34 |
+
class Accessor extends login_security_solution_admin {
|
35 |
+
/**
|
36 |
+
* This plugin's table name prefix FOR TESTING
|
37 |
+
* @var string
|
38 |
+
*/
|
39 |
+
protected $prefix = 'login_security_solution__tests__';
|
40 |
+
|
41 |
+
/**
|
42 |
+
* Is this class being used by our unit tests?
|
43 |
+
* @var bool
|
44 |
+
*/
|
45 |
+
protected $testing = true;
|
46 |
+
|
47 |
+
|
48 |
+
public function __call($method, $args) {
|
49 |
+
return call_user_func_array(array($this, $method), $args);
|
50 |
+
}
|
51 |
+
public function __get($property) {
|
52 |
+
return $this->$property;
|
53 |
+
}
|
54 |
+
public function __set($property, $value) {
|
55 |
+
$this->$property = $value;
|
56 |
+
}
|
57 |
+
public function get_data_element($key) {
|
58 |
+
return $this->data[$key];
|
59 |
+
}
|
60 |
+
}
|
tests/DisableLoginTest.php
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Test the disable login functionality
|
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 |
+
* Test the disable login functionality
|
19 |
+
*
|
20 |
+
* @package login-security-solution
|
21 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
22 |
+
* @copyright The Analysis and Solutions Company, 2012
|
23 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
24 |
+
*/
|
25 |
+
class DisableLoginTest extends TestCase {
|
26 |
+
public static function setUpBeforeClass() {
|
27 |
+
parent::$db_needed = true;
|
28 |
+
parent::set_up_before_class();
|
29 |
+
}
|
30 |
+
|
31 |
+
|
32 |
+
public function test_disable_login__false() {
|
33 |
+
$options = self::$lss->options;
|
34 |
+
$options['disable_logins'] = 0;
|
35 |
+
self::$lss->options = $options;
|
36 |
+
|
37 |
+
$actual = self::$lss->check(array(), $this->user);
|
38 |
+
$this->assertTrue($actual, 'Bad return value.');
|
39 |
+
}
|
40 |
+
|
41 |
+
public function test_disable_login__true() {
|
42 |
+
$options = self::$lss->options;
|
43 |
+
$options['disable_logins'] = 1;
|
44 |
+
self::$lss->options = $options;
|
45 |
+
|
46 |
+
$expected_error = 'Cannot modify header information';
|
47 |
+
$this->expected_errors($expected_error);
|
48 |
+
self::$location_expected = get_option('siteurl')
|
49 |
+
. '/wp-login.php?action=login';
|
50 |
+
|
51 |
+
$actual = self::$lss->check(array(), $this->user);
|
52 |
+
|
53 |
+
$this->assertTrue($this->were_expected_errors_found(),
|
54 |
+
"Expected error not found: '$expected_error'");
|
55 |
+
$this->assertEquals(self::$location_expected, self::$location_actual,
|
56 |
+
'wp_redirect() produced unexpected location header.');
|
57 |
+
|
58 |
+
$this->assertSame(-4, $actual, 'Bad return value.');
|
59 |
+
}
|
60 |
+
|
61 |
+
public function test_disable_login__true_but_admin() {
|
62 |
+
global $current_user;
|
63 |
+
|
64 |
+
$options = self::$lss->options;
|
65 |
+
$options['disable_logins'] = 1;
|
66 |
+
self::$lss->options = $options;
|
67 |
+
|
68 |
+
wp_set_current_user(1);
|
69 |
+
|
70 |
+
$actual = self::$lss->check(array(), $this->user);
|
71 |
+
$this->assertTrue($actual, 'Bad return value.');
|
72 |
+
}
|
73 |
+
}
|
tests/IdleTest.php
ADDED
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Test the idle timeout functionality
|
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 |
+
* Test the idle timeout functionality
|
19 |
+
*
|
20 |
+
* @package login-security-solution
|
21 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
22 |
+
* @copyright The Analysis and Solutions Company, 2012
|
23 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
24 |
+
*/
|
25 |
+
class IdleTest extends TestCase {
|
26 |
+
public static function setUpBeforeClass() {
|
27 |
+
parent::$db_needed = true;
|
28 |
+
parent::set_up_before_class();
|
29 |
+
}
|
30 |
+
|
31 |
+
|
32 |
+
public function test_get_last_active__empty_1() {
|
33 |
+
$actual = self::$lss->get_last_active($this->user->ID);
|
34 |
+
$this->assertSame(0, $actual);
|
35 |
+
}
|
36 |
+
|
37 |
+
public function test_set_last_active__add() {
|
38 |
+
$actual = self::$lss->set_last_active($this->user->ID);
|
39 |
+
$this->assertInternalType('integer', $actual, 'Bad return value.');
|
40 |
+
}
|
41 |
+
|
42 |
+
/**
|
43 |
+
* @depends test_set_last_active__add
|
44 |
+
*/
|
45 |
+
public function test_set_last_active__update() {
|
46 |
+
sleep(1);
|
47 |
+
$actual = self::$lss->set_last_active($this->user->ID);
|
48 |
+
$this->assertTrue($actual, 'Bad return value.');
|
49 |
+
}
|
50 |
+
|
51 |
+
/**
|
52 |
+
* @depends test_set_last_active__update
|
53 |
+
*/
|
54 |
+
public function test_get_last_active__something() {
|
55 |
+
$actual = self::$lss->get_last_active($this->user->ID);
|
56 |
+
$diff = (time() - $actual) < 1;
|
57 |
+
$this->assertGreaterThanOrEqual(0, $diff, 'Time was too long ago.');
|
58 |
+
$this->assertLessThanOrEqual(1, $diff, 'Time was in the future.');
|
59 |
+
}
|
60 |
+
|
61 |
+
/**
|
62 |
+
* @depends test_get_last_active__something
|
63 |
+
*/
|
64 |
+
public function test_delete_last_active__1() {
|
65 |
+
global $user_ID;
|
66 |
+
$user_ID = $this->user->ID;
|
67 |
+
$actual = self::$lss->delete_last_active();
|
68 |
+
$this->assertTrue($actual);
|
69 |
+
}
|
70 |
+
|
71 |
+
/**
|
72 |
+
* @depends test_delete_last_active__1
|
73 |
+
*/
|
74 |
+
public function test_get_last_active__empty_2() {
|
75 |
+
$actual = self::$lss->get_last_active($this->user->ID);
|
76 |
+
$this->assertSame(0, $actual);
|
77 |
+
}
|
78 |
+
|
79 |
+
public function test_delete_last_active__null_both() {
|
80 |
+
global $user_ID, $user_name;
|
81 |
+
$user_ID = null;
|
82 |
+
$user_name = null;
|
83 |
+
$actual = self::$lss->delete_last_active();
|
84 |
+
$this->assertNull($actual, 'Bad return value.');
|
85 |
+
}
|
86 |
+
|
87 |
+
public function test_delete_last_active__user_name() {
|
88 |
+
global $user_ID, $user_name, $wpdb;
|
89 |
+
|
90 |
+
$actual = $wpdb->insert(
|
91 |
+
$wpdb->users,
|
92 |
+
array(
|
93 |
+
'user_login' => $this->user->user_login,
|
94 |
+
)
|
95 |
+
);
|
96 |
+
$this->assertSame(1, $actual, 'Could not insert sample record.');
|
97 |
+
|
98 |
+
$actual = self::$lss->set_last_active($wpdb->insert_id);
|
99 |
+
$this->assertInternalType('integer', $actual, 'Set last active...');
|
100 |
+
|
101 |
+
$user_ID = null;
|
102 |
+
$user_name = $this->user->user_login;
|
103 |
+
$actual = self::$lss->delete_last_active();
|
104 |
+
$this->assertTrue($actual, 'Delete last active...');
|
105 |
+
}
|
106 |
+
|
107 |
+
/*
|
108 |
+
* IS IDLE
|
109 |
+
*/
|
110 |
+
|
111 |
+
public function test_is_idle__off() {
|
112 |
+
$options = self::$lss->options;
|
113 |
+
$options['idle_timeout'] = 0;
|
114 |
+
self::$lss->options = $options;
|
115 |
+
|
116 |
+
$actual = self::$lss->is_idle($this->user->ID);
|
117 |
+
$this->assertNull($actual);
|
118 |
+
}
|
119 |
+
|
120 |
+
/**
|
121 |
+
* @depends test_delete_last_active__user_name
|
122 |
+
*/
|
123 |
+
public function test_is_idle__add() {
|
124 |
+
$options = self::$lss->options;
|
125 |
+
$options['idle_timeout'] = 15;
|
126 |
+
self::$lss->options = $options;
|
127 |
+
|
128 |
+
$actual = self::$lss->is_idle($this->user->ID);
|
129 |
+
$this->assertSame(0, $actual, 'Bad return value.');
|
130 |
+
|
131 |
+
$actual = self::$lss->get_last_active($this->user->ID);
|
132 |
+
$diff = (time() - $actual) < 1;
|
133 |
+
$this->assertGreaterThanOrEqual(0, $diff, 'Time was too long ago.');
|
134 |
+
$this->assertLessThanOrEqual(1, $diff, 'Time was in the future.');
|
135 |
+
}
|
136 |
+
|
137 |
+
/**
|
138 |
+
* @depends test_is_idle__add
|
139 |
+
*/
|
140 |
+
public function test_is_idle__update() {
|
141 |
+
$options = self::$lss->options;
|
142 |
+
$options['idle_timeout'] = 1;
|
143 |
+
self::$lss->options = $options;
|
144 |
+
|
145 |
+
$actual = self::$lss->is_idle($this->user->ID);
|
146 |
+
$this->assertFalse($actual);
|
147 |
+
}
|
148 |
+
|
149 |
+
/**
|
150 |
+
* @depends test_is_idle__update
|
151 |
+
*/
|
152 |
+
public function test_is_idle__true() {
|
153 |
+
$options = self::$lss->options;
|
154 |
+
$options['idle_timeout'] = -1;
|
155 |
+
self::$lss->options = $options;
|
156 |
+
|
157 |
+
$actual = self::$lss->is_idle($this->user->ID);
|
158 |
+
$this->assertTrue($actual);
|
159 |
+
}
|
160 |
+
|
161 |
+
/*
|
162 |
+
* CHECK
|
163 |
+
*/
|
164 |
+
|
165 |
+
/**
|
166 |
+
* @depends test_delete_last_active__user_name
|
167 |
+
*/
|
168 |
+
public function test_check__empty_user_id() {
|
169 |
+
$this->user->ID = null;
|
170 |
+
$actual = self::$lss->check(array(), $this->user);
|
171 |
+
$this->assertFalse($actual, 'Bad return value.');
|
172 |
+
}
|
173 |
+
|
174 |
+
/**
|
175 |
+
* @depends test_delete_last_active__user_name
|
176 |
+
*/
|
177 |
+
public function test_check__empty() {
|
178 |
+
$actual = self::$lss->check(array(), null);
|
179 |
+
$this->assertFalse($actual, 'Bad return value.');
|
180 |
+
}
|
181 |
+
|
182 |
+
/**
|
183 |
+
* @depends test_delete_last_active__user_name
|
184 |
+
*/
|
185 |
+
public function test_check__non_user() {
|
186 |
+
$other = new stdClass;
|
187 |
+
$actual = self::$lss->check(array(), $other);
|
188 |
+
$this->assertFalse($actual, 'Bad return value.');
|
189 |
+
}
|
190 |
+
|
191 |
+
/**
|
192 |
+
* @depends test_delete_last_active__user_name
|
193 |
+
*/
|
194 |
+
public function test_check__okay() {
|
195 |
+
$options = self::$lss->options;
|
196 |
+
$options['idle_timeout'] = 1;
|
197 |
+
self::$lss->options = $options;
|
198 |
+
|
199 |
+
$actual = self::$lss->check(array(), $this->user);
|
200 |
+
$this->assertTrue($actual);
|
201 |
+
}
|
202 |
+
|
203 |
+
/**
|
204 |
+
* @depends test_check__okay
|
205 |
+
*/
|
206 |
+
public function test_check__fail() {
|
207 |
+
$options = self::$lss->options;
|
208 |
+
$options['idle_timeout'] = -1;
|
209 |
+
self::$lss->options = $options;
|
210 |
+
|
211 |
+
$expected_error = 'Cannot modify header information';
|
212 |
+
$this->expected_errors($expected_error);
|
213 |
+
self::$location_expected = get_option('siteurl')
|
214 |
+
. '/wp-login.php?action=login&'
|
215 |
+
. self::$lss->key_login_msg . '=idle';
|
216 |
+
|
217 |
+
$actual = self::$lss->check(array(), $this->user);
|
218 |
+
|
219 |
+
$this->assertTrue($this->were_expected_errors_found(),
|
220 |
+
"Expected error not found: '$expected_error'");
|
221 |
+
$this->assertEquals(self::$location_expected, self::$location_actual,
|
222 |
+
'wp_redirect() produced unexpected location header.');
|
223 |
+
|
224 |
+
$this->assertSame(-5, $actual, 'Bad return value.');
|
225 |
+
}
|
226 |
+
}
|
tests/IpTest.php
ADDED
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Test IP related methods
|
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 |
+
* Test IP related methods
|
19 |
+
*
|
20 |
+
* @package login-security-solution
|
21 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
22 |
+
* @copyright The Analysis and Solutions Company, 2012
|
23 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
24 |
+
*/
|
25 |
+
class IpTest extends TestCase {
|
26 |
+
public static function setUpBeforeClass() {
|
27 |
+
parent::$db_needed = false;
|
28 |
+
parent::set_up_before_class();
|
29 |
+
}
|
30 |
+
|
31 |
+
|
32 |
+
public function data_ipv4() {
|
33 |
+
return array(
|
34 |
+
array('good' => array('1.2.3.4' => '1.2.3.4')),
|
35 |
+
array('zerofill' => array('005.006.007.008' => '5.6.7.8')),
|
36 |
+
array('big' => array('1.256.3.4' => '')),
|
37 |
+
array('short' => array('1.2.3' => '')),
|
38 |
+
array('long' => array('1.2.3.4.5' => '')),
|
39 |
+
array('text' => array('abcd' => '')),
|
40 |
+
array('empty' => array('' => '')),
|
41 |
+
array('trim empty' => array(' ' => '')),
|
42 |
+
);
|
43 |
+
}
|
44 |
+
|
45 |
+
public function data_ipv6() {
|
46 |
+
return array(
|
47 |
+
array('good' => array('1:2:3:4:5:6:7:8' => '1:2:3:4:5:6:7:8')),
|
48 |
+
array('zerofill' => array('01:002:0003:4:5:6:7:8' => '1:2:3:4:5:6:7:8')),
|
49 |
+
array('hex' => array('FFFF:D1D1:3:4:5:6:7:8' => 'ffff:d1d1:3:4:5:6:7:8')),
|
50 |
+
// Not a kosher input format, but make it work anyway.
|
51 |
+
array('compress mid 1' => array('1:2:3:4::6:7:8' => '1:2:3:4:0:6:7:8')),
|
52 |
+
array('compress mid 2' => array('1:2:3::6:7:8' => '1:2:3:0:0:6:7:8')),
|
53 |
+
array('compress mid 3' => array('1:2::6:7:8' => '1:2:0:0:0:6:7:8')),
|
54 |
+
// Not a kosher input format, but make it work anyway.
|
55 |
+
array('compress left 1' => array('::2:3:4:5:6:7:8' => '0:2:3:4:5:6:7:8')),
|
56 |
+
array('compress left 2' => array('::3:4:5:6:7:8' => '0:0:3:4:5:6:7:8')),
|
57 |
+
array('compress left 3' => array('::4:5:6:7:8' => '0:0:0:4:5:6:7:8')),
|
58 |
+
// Not a kosher input format, but make it work anyway.
|
59 |
+
array('compress mid 1' => array('1:2:3:4::6:7:8' => '1:2:3:4:0:6:7:8')),
|
60 |
+
array('compress right 1' => array('1:2:3:4:5:6:7::' => '1:2:3:4:5:6:7:0')),
|
61 |
+
array('compress right 2' => array('1:2:3:4:5:6::' => '1:2:3:4:5:6:0:0')),
|
62 |
+
array('compress right 3' => array('1:2:3:4:5::' => '1:2:3:4:5:0:0:0')),
|
63 |
+
array('too many 1' => array('1:2:3:4:5:6:7:8:9' => '')),
|
64 |
+
array('too few 1' => array('1:2:3:4:5:6:7' => '')),
|
65 |
+
array('too few 2' => array('1:2:3:4:5:6' => '')),
|
66 |
+
array('too many compressions' => array('1:2:::8' => '')),
|
67 |
+
array('empty' => array('' => '')),
|
68 |
+
array('unspecified' => array('::' => '')),
|
69 |
+
array('text' => array('abcd' => '')),
|
70 |
+
array('hextobig' => array('FFFF:D1D1D1:3:4:5:6:7:8' => '')),
|
71 |
+
array('inttobig' => array('FFFF:D1D1:3:40000:5:6:7:8' => '')),
|
72 |
+
array('embed compat' => array('::1.2.3.4' => '0:0:0:0:0:0:1.2.3.4')),
|
73 |
+
array('embed compat filled' => array('0:0:0:0:0:0:1.2.3.4' => '0:0:0:0:0:0:1.2.3.4')),
|
74 |
+
array('embed map' => array('::ffff:1.2.3.4' => '0:0:0:0:0:ffff:1.2.3.4')),
|
75 |
+
array('embed bad prefix' => array('::acac:1.2.3.4' => '')),
|
76 |
+
array('embed map bad 4' => array('::ffff:1.2.666.4' => '')),
|
77 |
+
);
|
78 |
+
}
|
79 |
+
|
80 |
+
/**
|
81 |
+
* @dataProvider data_ipv4
|
82 |
+
*/
|
83 |
+
public function test_normalize_ipv4($data) {
|
84 |
+
list($input, $expect) = each($data);
|
85 |
+
$actual = self::$lss->normalize_ipv4($input);
|
86 |
+
$this->assertEquals($expect, $actual);
|
87 |
+
}
|
88 |
+
/**
|
89 |
+
* @dataProvider data_ipv6
|
90 |
+
*/
|
91 |
+
public function test_normalize_ipv6($data) {
|
92 |
+
list($input, $expect) = each($data);
|
93 |
+
$actual = self::$lss->normalize_ipv6($input);
|
94 |
+
$this->assertEquals($expect, $actual);
|
95 |
+
}
|
96 |
+
|
97 |
+
/**#@+
|
98 |
+
* get_ip()
|
99 |
+
*/
|
100 |
+
/**
|
101 |
+
* @dataProvider data_ipv4
|
102 |
+
*/
|
103 |
+
public function test_get_ip__ipv4($data) {
|
104 |
+
list($input, $expect) = each($data);
|
105 |
+
$_SERVER['REMOTE_ADDR'] = $input;
|
106 |
+
$actual = self::$lss->get_ip();
|
107 |
+
$this->assertEquals($expect, $actual);
|
108 |
+
}
|
109 |
+
public function test_get_ip__ipv4_array() {
|
110 |
+
$_SERVER['REMOTE_ADDR'] = array('foo');
|
111 |
+
$actual = self::$lss->get_ip();
|
112 |
+
$this->assertEquals('', $actual);
|
113 |
+
}
|
114 |
+
/**
|
115 |
+
* @dataProvider data_ipv6
|
116 |
+
*/
|
117 |
+
public function test_get_ip__ipv6($data) {
|
118 |
+
list($input, $expect) = each($data);
|
119 |
+
$_SERVER['REMOTE_ADDR'] = $input;
|
120 |
+
$actual = self::$lss->get_ip();
|
121 |
+
$this->assertEquals($expect, $actual);
|
122 |
+
}
|
123 |
+
/**#@- **/
|
124 |
+
|
125 |
+
/**#@+
|
126 |
+
* get_network_ip()
|
127 |
+
*/
|
128 |
+
public function test_get_network_ip__empty() {
|
129 |
+
$_SERVER['REMOTE_ADDR'] = '';
|
130 |
+
$actual = self::$lss->get_network_ip();
|
131 |
+
$this->assertEquals('', $actual);
|
132 |
+
}
|
133 |
+
public function test_get_network_ip__array_remote() {
|
134 |
+
$_SERVER['REMOTE_ADDR'] = array('1.2.3.4');
|
135 |
+
$actual = self::$lss->get_network_ip();
|
136 |
+
$this->assertEquals('', $actual);
|
137 |
+
}
|
138 |
+
public function test_get_network_ip__array_param() {
|
139 |
+
$actual = self::$lss->get_network_ip(array('1.2.3.4'));
|
140 |
+
$this->assertEquals('', $actual);
|
141 |
+
}
|
142 |
+
public function test_get_network_ip__ipv4() {
|
143 |
+
$actual = self::$lss->get_network_ip('1.2.3.4');
|
144 |
+
$this->assertEquals('1.2.3', $actual);
|
145 |
+
}
|
146 |
+
public function test_get_network_ip__ipv4_remote_addr() {
|
147 |
+
$_SERVER['REMOTE_ADDR'] = '1.2.3.4';
|
148 |
+
$actual = self::$lss->get_network_ip();
|
149 |
+
$this->assertEquals('1.2.3', $actual);
|
150 |
+
}
|
151 |
+
public function test_get_network_ip__ipv6() {
|
152 |
+
$actual = self::$lss->get_network_ip('1:2:3:4:5:6:7:8');
|
153 |
+
$this->assertEquals('1:2:3:4', $actual);
|
154 |
+
}
|
155 |
+
public function test_get_network_ip__ipv6_remote_addr() {
|
156 |
+
$_SERVER['REMOTE_ADDR'] = '1:2:3:4:5:6:7:8';
|
157 |
+
$actual = self::$lss->get_network_ip();
|
158 |
+
$this->assertEquals('1:2:3:4', $actual);
|
159 |
+
}
|
160 |
+
/**#@- **/
|
161 |
+
}
|
tests/LoginErrorsTest.php
ADDED
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Test the login errors filter
|
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 |
+
* Test the login errors filter
|
19 |
+
*
|
20 |
+
* @package login-security-solution
|
21 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
22 |
+
* @copyright The Analysis and Solutions Company, 2012
|
23 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
24 |
+
*/
|
25 |
+
class LoginErrorsTest extends TestCase {
|
26 |
+
protected $ip;
|
27 |
+
protected $network_ip;
|
28 |
+
protected $user_name;
|
29 |
+
protected $user_pass;
|
30 |
+
protected $pass_md5;
|
31 |
+
|
32 |
+
|
33 |
+
public static function setUpBeforeClass() {
|
34 |
+
parent::$db_needed = true;
|
35 |
+
parent::set_up_before_class();
|
36 |
+
}
|
37 |
+
|
38 |
+
public function setUp() {
|
39 |
+
global $errors;
|
40 |
+
|
41 |
+
parent::setUp();
|
42 |
+
|
43 |
+
$this->ip = '1.2.3.4';
|
44 |
+
$_SERVER['REMOTE_ADDR'] = $this->ip;
|
45 |
+
$this->network_ip = '1.2.3';
|
46 |
+
|
47 |
+
$this->user_name = 'test';
|
48 |
+
$this->user_pass = 'ababab';
|
49 |
+
$this->pass_md5 = self::$lss->md5($this->user_pass);
|
50 |
+
|
51 |
+
$options = self::$lss->options;
|
52 |
+
$options['login_fail_minutes'] = 60;
|
53 |
+
$options['login_fail_notify'] = 0;
|
54 |
+
$options['login_fail_breach_notify'] = 0;
|
55 |
+
self::$lss->options = $options;
|
56 |
+
|
57 |
+
$errors = new WP_Error;
|
58 |
+
$_POST['log'] = 'username';
|
59 |
+
$_POST['pwd'] = $this->user_pass;
|
60 |
+
unset($_REQUEST['action']);
|
61 |
+
}
|
62 |
+
|
63 |
+
|
64 |
+
public function test_login_errors__nothing() {
|
65 |
+
global $errors, $user_name;
|
66 |
+
|
67 |
+
$actual = self::$lss->login_errors('input');
|
68 |
+
$this->assertEquals('input', $actual, 'Output should not be modified.');
|
69 |
+
$this->assertArrayHasKey('log', $_POST, "POST log shouldn't be touched.");
|
70 |
+
}
|
71 |
+
|
72 |
+
public function test_login_errors__register() {
|
73 |
+
global $errors, $user_name;
|
74 |
+
|
75 |
+
$errors->add('invalid_username', 'blargh');
|
76 |
+
$_REQUEST['action'] = 'register';
|
77 |
+
|
78 |
+
$actual = self::$lss->login_errors('input');
|
79 |
+
$this->assertEquals('input',
|
80 |
+
$actual, 'Output should not be modified.');
|
81 |
+
$this->assertArrayHasKey('log', $_POST, "POST log shouldn't be touched.");
|
82 |
+
}
|
83 |
+
|
84 |
+
public function test_login_errors__bad_name() {
|
85 |
+
global $errors, $user_name;
|
86 |
+
|
87 |
+
$errors->add('invalid_username', 'blargh');
|
88 |
+
$user_name = $this->user_name;
|
89 |
+
|
90 |
+
$actual = self::$lss->login_errors('input');
|
91 |
+
|
92 |
+
$this->assertEquals('Invalid username or password.',
|
93 |
+
$actual, 'Output should have been modified.');
|
94 |
+
$this->assertArrayNotHasKey('log', $_POST, "POST log should be unset.");
|
95 |
+
}
|
96 |
+
|
97 |
+
public function test_login_errors__bad_pw() {
|
98 |
+
global $errors, $user_name;
|
99 |
+
|
100 |
+
$errors->add('incorrect_password', 'blargh');
|
101 |
+
$user_name = $this->user_name;
|
102 |
+
|
103 |
+
$actual = self::$lss->login_errors('input');
|
104 |
+
|
105 |
+
$this->assertEquals('Invalid username or password.',
|
106 |
+
$actual, 'Output should have been modified.');
|
107 |
+
$this->assertArrayNotHasKey('log', $_POST, "POST log should be unset.");
|
108 |
+
}
|
109 |
+
|
110 |
+
public function test_login_errors__reset_bad_email() {
|
111 |
+
global $errors, $user_name;
|
112 |
+
|
113 |
+
$errors->add('invalid_email', 'blargh');
|
114 |
+
|
115 |
+
$actual = self::$lss->login_errors('input');
|
116 |
+
|
117 |
+
$this->assertEquals(__('Password reset is not allowed for this user'),
|
118 |
+
$actual, 'Output should have been modified.');
|
119 |
+
$this->assertArrayHasKey('log', $_POST, "POST log shouldn't be touched.");
|
120 |
+
}
|
121 |
+
|
122 |
+
public function test_login_errors__reset_bad_combo() {
|
123 |
+
global $errors, $user_name;
|
124 |
+
|
125 |
+
$errors->add('invalidcombo', 'blargh');
|
126 |
+
|
127 |
+
$actual = self::$lss->login_errors('input');
|
128 |
+
|
129 |
+
$this->assertEquals(__('Password reset is not allowed for this user'),
|
130 |
+
$actual, 'Output should have been modified.');
|
131 |
+
$this->assertArrayHasKey('log', $_POST, "POST log shouldn't be touched.");
|
132 |
+
}
|
133 |
+
}
|
tests/LoginFailTest.php
ADDED
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Test login failure and lockout functionality
|
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 |
+
* Test login failure and lockout functionality
|
19 |
+
*
|
20 |
+
* @package login-security-solution
|
21 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
22 |
+
* @copyright The Analysis and Solutions Company, 2012
|
23 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
24 |
+
*/
|
25 |
+
class LoginFailTest extends TestCase {
|
26 |
+
protected $ip;
|
27 |
+
protected $network_ip;
|
28 |
+
protected $user_name;
|
29 |
+
protected $pass_md5;
|
30 |
+
|
31 |
+
|
32 |
+
public static function setUpBeforeClass() {
|
33 |
+
parent::$db_needed = true;
|
34 |
+
parent::set_up_before_class();
|
35 |
+
}
|
36 |
+
|
37 |
+
public function setUp() {
|
38 |
+
parent::setUp();
|
39 |
+
|
40 |
+
$this->ip = '1.2.3.4';
|
41 |
+
$_SERVER['REMOTE_ADDR'] = $this->ip;
|
42 |
+
$this->network_ip = '1.2.3';
|
43 |
+
|
44 |
+
$this->user_name = 'test';
|
45 |
+
$this->pass_md5 = 'ababab';
|
46 |
+
|
47 |
+
$options = self::$lss->options;
|
48 |
+
$options['login_fail_minutes'] = 60;
|
49 |
+
$options['login_fail_notify'] = 4;
|
50 |
+
$options['login_fail_tier_2'] = 3;
|
51 |
+
$options['login_fail_tier_3'] = 4;
|
52 |
+
$options['login_fail_breach_notify'] = 4;
|
53 |
+
$options['login_fail_breach_pw_force_change'] = 4;
|
54 |
+
self::$lss->options = $options;
|
55 |
+
}
|
56 |
+
|
57 |
+
|
58 |
+
/*
|
59 |
+
* LOGIN FAIL
|
60 |
+
*/
|
61 |
+
|
62 |
+
public function test_insert_fail() {
|
63 |
+
self::$lss->insert_fail($this->ip, $this->user_name, $this->pass_md5);
|
64 |
+
$this->check_fail_record($this->ip, $this->user_name, $this->pass_md5);
|
65 |
+
|
66 |
+
self::$lss->insert_fail($this->ip, $this->user_name, 'other md5');
|
67 |
+
$this->check_fail_record($this->ip, $this->user_name, 'other md5');
|
68 |
+
}
|
69 |
+
|
70 |
+
/**
|
71 |
+
* @depends test_insert_fail
|
72 |
+
*/
|
73 |
+
public function test_get_login_fail() {
|
74 |
+
$expected = array(
|
75 |
+
'total' => '2',
|
76 |
+
'network_ip' => '2',
|
77 |
+
'user_name' => '2',
|
78 |
+
'pass_md5' => '1',
|
79 |
+
);
|
80 |
+
|
81 |
+
$actual = self::$lss->get_login_fail($this->network_ip,
|
82 |
+
$this->user_name, $this->pass_md5);
|
83 |
+
|
84 |
+
$this->assertEquals($expected, $actual);
|
85 |
+
}
|
86 |
+
|
87 |
+
/**
|
88 |
+
* @depends test_insert_fail
|
89 |
+
*/
|
90 |
+
public function test_get_login_fail__empty_ip() {
|
91 |
+
global $wpdb;
|
92 |
+
|
93 |
+
$expected = array(
|
94 |
+
'total' => '3',
|
95 |
+
'network_ip' => '1',
|
96 |
+
'user_name' => '3',
|
97 |
+
'pass_md5' => '2',
|
98 |
+
);
|
99 |
+
|
100 |
+
$wpdb->query('SAVEPOINT pre_threshold');
|
101 |
+
|
102 |
+
self::$lss->insert_fail('', $this->user_name, $this->pass_md5);
|
103 |
+
$this->check_fail_record('', $this->user_name, $this->pass_md5);
|
104 |
+
|
105 |
+
$actual = self::$lss->get_login_fail('',
|
106 |
+
$this->user_name, $this->pass_md5);
|
107 |
+
|
108 |
+
$this->assertEquals($expected, $actual);
|
109 |
+
|
110 |
+
$wpdb->query('ROLLBACK TO pre_threshold');
|
111 |
+
}
|
112 |
+
|
113 |
+
/*
|
114 |
+
* PROCESS LOGIN FAIL
|
115 |
+
*/
|
116 |
+
|
117 |
+
/**
|
118 |
+
* @depends test_get_login_fail
|
119 |
+
*/
|
120 |
+
public function test_process_login_fail__pre_threshold() {
|
121 |
+
global $wpdb;
|
122 |
+
|
123 |
+
self::$lss->process_login_fail($this->user_name, $this->pass_md5);
|
124 |
+
|
125 |
+
$this->assertInternalType('integer', $wpdb->insert_id,
|
126 |
+
'This should be an insert id.');
|
127 |
+
}
|
128 |
+
|
129 |
+
public function test_wp_login__null() {
|
130 |
+
$actual = self::$lss->wp_login(null, null);
|
131 |
+
$this->assertNull($actual, 'Bad return value.');
|
132 |
+
}
|
133 |
+
|
134 |
+
/**
|
135 |
+
* @depends test_process_login_fail__pre_threshold
|
136 |
+
*/
|
137 |
+
public function test_wp_login__pre_breach_threshold() {
|
138 |
+
$actual = self::$lss->wp_login($this->user_name, $this->user);
|
139 |
+
$this->assertSame(1, $actual, 'wp_login() return value...');
|
140 |
+
|
141 |
+
$actual = self::$lss->get_pw_force_change($this->user->ID);
|
142 |
+
$this->assertFalse($actual, 'get_pw_force_change() return value...');
|
143 |
+
}
|
144 |
+
|
145 |
+
/**
|
146 |
+
* @depends test_process_login_fail__pre_threshold
|
147 |
+
*/
|
148 |
+
public function test_process_login_fail__post_threshold() {
|
149 |
+
self::$mail_file_basename = __METHOD__;
|
150 |
+
|
151 |
+
try {
|
152 |
+
// Do THE deed.
|
153 |
+
self::$lss->process_login_fail($this->user_name, $this->pass_md5);
|
154 |
+
} catch (Exception $e) {
|
155 |
+
$this->fail($e->getMessage());
|
156 |
+
}
|
157 |
+
|
158 |
+
$this->check_mail_file();
|
159 |
+
}
|
160 |
+
|
161 |
+
/**
|
162 |
+
* @depends test_process_login_fail__post_threshold
|
163 |
+
*/
|
164 |
+
public function test_wp_login__post_breach_threshold() {
|
165 |
+
self::$mail_file_basename = __METHOD__;
|
166 |
+
|
167 |
+
try {
|
168 |
+
// Do THE deed.
|
169 |
+
$actual = self::$lss->wp_login($this->user_name, $this->user);
|
170 |
+
} catch (Exception $e) {
|
171 |
+
$this->fail($e->getMessage());
|
172 |
+
}
|
173 |
+
$this->assertSame(7, $actual, 'Bad return value.');
|
174 |
+
|
175 |
+
$actual = self::$lss->get_pw_force_change($this->user->ID);
|
176 |
+
$this->assertTrue($actual, 'get_pw_force_change() return value...');
|
177 |
+
|
178 |
+
$this->check_mail_file();
|
179 |
+
}
|
180 |
+
|
181 |
+
/**
|
182 |
+
* @depends test_process_login_fail__post_threshold
|
183 |
+
*/
|
184 |
+
public function test_wp_login__post_breach_threshold_only_notify() {
|
185 |
+
$options = self::$lss->options;
|
186 |
+
$options['login_fail_breach_pw_force_change'] = 0;
|
187 |
+
self::$lss->options = $options;
|
188 |
+
|
189 |
+
self::$lss->delete_pw_force_change($this->user->ID);
|
190 |
+
self::$mail_file_basename = 'LoginFailTest::test_wp_login__post_breach_threshold';
|
191 |
+
|
192 |
+
try {
|
193 |
+
// Do THE deed.
|
194 |
+
$actual = self::$lss->wp_login($this->user_name, $this->user);
|
195 |
+
} catch (Exception $e) {
|
196 |
+
$this->fail($e->getMessage());
|
197 |
+
}
|
198 |
+
$this->assertSame(5, $actual, 'Bad return value.');
|
199 |
+
|
200 |
+
$actual = self::$lss->get_pw_force_change($this->user->ID);
|
201 |
+
$this->assertFalse($actual, 'get_pw_force_change() return value...');
|
202 |
+
|
203 |
+
$this->check_mail_file();
|
204 |
+
}
|
205 |
+
|
206 |
+
/**
|
207 |
+
* @depends test_process_login_fail__post_threshold
|
208 |
+
*/
|
209 |
+
public function test_wp_login__post_breach_threshold_only_force() {
|
210 |
+
$options = self::$lss->options;
|
211 |
+
$options['login_fail_breach_notify'] = 0;
|
212 |
+
self::$lss->options = $options;
|
213 |
+
|
214 |
+
self::$lss->delete_pw_force_change($this->user->ID);
|
215 |
+
|
216 |
+
try {
|
217 |
+
// Do THE deed.
|
218 |
+
$actual = self::$lss->wp_login($this->user_name, $this->user);
|
219 |
+
} catch (Exception $e) {
|
220 |
+
$this->fail($e->getMessage());
|
221 |
+
}
|
222 |
+
$this->assertSame(3, $actual, 'Bad return value.');
|
223 |
+
|
224 |
+
$actual = self::$lss->get_pw_force_change($this->user->ID);
|
225 |
+
$this->assertTrue($actual, 'get_pw_force_change() return value...');
|
226 |
+
}
|
227 |
+
|
228 |
+
/**
|
229 |
+
* @depends test_process_login_fail__post_threshold
|
230 |
+
*/
|
231 |
+
public function test_wp_login__post_breach_threshold_no_action() {
|
232 |
+
$options = self::$lss->options;
|
233 |
+
$options['login_fail_breach_notify'] = 0;
|
234 |
+
$options['login_fail_breach_pw_force_change'] = 0;
|
235 |
+
self::$lss->options = $options;
|
236 |
+
|
237 |
+
self::$lss->delete_pw_force_change($this->user->ID);
|
238 |
+
|
239 |
+
try {
|
240 |
+
// Do THE deed.
|
241 |
+
$actual = self::$lss->wp_login($this->user_name, $this->user);
|
242 |
+
} catch (Exception $e) {
|
243 |
+
$this->fail($e->getMessage());
|
244 |
+
}
|
245 |
+
$this->assertSame(-1, $actual, 'Bad return value.');
|
246 |
+
|
247 |
+
$actual = self::$lss->get_pw_force_change($this->user->ID);
|
248 |
+
$this->assertFalse($actual, 'get_pw_force_change() return value...');
|
249 |
+
}
|
250 |
+
}
|
tests/LoginMessageTest.php
ADDED
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Test the login message filter
|
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 |
+
* Test the login message filter
|
19 |
+
*
|
20 |
+
* @package login-security-solution
|
21 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
22 |
+
* @copyright The Analysis and Solutions Company, 2012
|
23 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
24 |
+
*/
|
25 |
+
class LoginMessageTest extends TestCase {
|
26 |
+
public static function setUpBeforeClass() {
|
27 |
+
parent::$db_needed = false;
|
28 |
+
parent::set_up_before_class();
|
29 |
+
}
|
30 |
+
|
31 |
+
|
32 |
+
public function ours($ours) {
|
33 |
+
return '<p class="login message">'
|
34 |
+
. htmlspecialchars($ours) . '</p>';
|
35 |
+
}
|
36 |
+
|
37 |
+
public function test_login_message__unset() {
|
38 |
+
unset($_GET[self::$lss->key_login_msg]);
|
39 |
+
|
40 |
+
$actual = self::$lss->login_message('input');
|
41 |
+
$this->assertEquals('input', $actual, 'Output should not be modified.');
|
42 |
+
}
|
43 |
+
|
44 |
+
public function test_login_message__empty() {
|
45 |
+
$_GET[self::$lss->key_login_msg] = '';
|
46 |
+
|
47 |
+
$actual = self::$lss->login_message('input');
|
48 |
+
$this->assertEquals('input', $actual, 'Output should not be modified.');
|
49 |
+
}
|
50 |
+
|
51 |
+
public function test_login_message__bogus() {
|
52 |
+
$_GET[self::$lss->key_login_msg] = 'ajkdslfasjdlfaskjdl';
|
53 |
+
|
54 |
+
$actual = self::$lss->login_message('input');
|
55 |
+
$this->assertEquals('input', $actual, 'Output should not be modified.');
|
56 |
+
}
|
57 |
+
|
58 |
+
public function test_login_message__idle() {
|
59 |
+
$_GET[self::$lss->key_login_msg] = 'idle';
|
60 |
+
|
61 |
+
$ours = sprintf(__('It has been over %d minutes since your last action.', self::ID), self::$lss->options['idle_timeout']);
|
62 |
+
$ours .= ' ' . __('Please log back in.', self::ID);
|
63 |
+
|
64 |
+
$actual = self::$lss->login_message('input');
|
65 |
+
$this->assertEquals('input' . $this->ours($ours), $actual,
|
66 |
+
'Output should have been modified.');
|
67 |
+
}
|
68 |
+
|
69 |
+
public function test_login_message__pw_expired() {
|
70 |
+
$_GET[self::$lss->key_login_msg] = 'pw_expired';
|
71 |
+
|
72 |
+
$ours = __('The grace period for changing your password has expired.', self::ID);
|
73 |
+
$ours .= ' ' . __('Please submit this form to reset your password.', self::ID);
|
74 |
+
|
75 |
+
$actual = self::$lss->login_message('input');
|
76 |
+
$this->assertEquals('input' . $this->ours($ours), $actual,
|
77 |
+
'Output should have been modified.');
|
78 |
+
}
|
79 |
+
|
80 |
+
public function test_login_message__pw_force() {
|
81 |
+
$_GET[self::$lss->key_login_msg] = 'pw_force';
|
82 |
+
|
83 |
+
$ours = __('Your password must be reset.', self::ID);
|
84 |
+
$ours .= ' ' . __('Please submit this form to reset it.', self::ID);
|
85 |
+
|
86 |
+
$actual = self::$lss->login_message('input');
|
87 |
+
$this->assertEquals('input' . $this->ours($ours), $actual,
|
88 |
+
'Output should have been modified.');
|
89 |
+
}
|
90 |
+
|
91 |
+
public function test_login_message__pw_grace() {
|
92 |
+
$_GET[self::$lss->key_login_msg] = 'pw_grace';
|
93 |
+
|
94 |
+
$ours = __('Your password has expired. Please log and change it.', self::ID);
|
95 |
+
$ours .= ' ' . sprintf(__('We provide a %d minute grace period to do so.', self::ID), self::$lss->options['pw_change_grace_period_minutes']);
|
96 |
+
|
97 |
+
$actual = self::$lss->login_message('input');
|
98 |
+
$this->assertEquals('input' . $this->ours($ours), $actual,
|
99 |
+
'Output should have been modified.');
|
100 |
+
}
|
101 |
+
|
102 |
+
public function test_login_message__disable_logins__no_key() {
|
103 |
+
$_GET[self::$lss->key_login_msg] = '';
|
104 |
+
|
105 |
+
$options = self::$lss->options;
|
106 |
+
$options['disable_logins'] = 1;
|
107 |
+
self::$lss->options = $options;
|
108 |
+
|
109 |
+
$ours = __('The site is undergoing maintenance.', self::ID);
|
110 |
+
$ours .= ' ' . __('Please try again later.', self::ID);
|
111 |
+
|
112 |
+
$actual = self::$lss->login_message('input');
|
113 |
+
$this->assertEquals('input' . $this->ours($ours), $actual,
|
114 |
+
'Output should have been modified.');
|
115 |
+
}
|
116 |
+
|
117 |
+
public function test_login_message__disable_logins__key() {
|
118 |
+
$_GET[self::$lss->key_login_msg] = 'pw_grace';
|
119 |
+
|
120 |
+
$options = self::$lss->options;
|
121 |
+
$options['disable_logins'] = 1;
|
122 |
+
self::$lss->options = $options;
|
123 |
+
|
124 |
+
$ours = __('The site is undergoing maintenance.', self::ID);
|
125 |
+
$ours .= ' ' . __('Please try again later.', self::ID);
|
126 |
+
|
127 |
+
$actual = self::$lss->login_message('input');
|
128 |
+
$this->assertEquals('input' . $this->ours($ours), $actual,
|
129 |
+
'Output should have been modified.');
|
130 |
+
}
|
131 |
+
}
|
tests/PasswordChangeTest.php
ADDED
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Test the behaviors when passwords are changed
|
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 |
+
* Test the behaviors when passwords are changed
|
19 |
+
*
|
20 |
+
* @package login-security-solution
|
21 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
22 |
+
* @copyright The Analysis and Solutions Company, 2012
|
23 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
24 |
+
*/
|
25 |
+
class PasswordChangeTest extends TestCase {
|
26 |
+
protected static $pass_1;
|
27 |
+
protected static $pass_2;
|
28 |
+
protected static $hash_1;
|
29 |
+
protected static $hash_2;
|
30 |
+
|
31 |
+
|
32 |
+
public static function setUpBeforeClass() {
|
33 |
+
parent::$db_needed = true;
|
34 |
+
parent::set_up_before_class();
|
35 |
+
|
36 |
+
self::$pass_1 = self::USER_PASS;
|
37 |
+
self::$pass_2 = '!AJd81aasjk2@';
|
38 |
+
self::$hash_1 = wp_hash_password(self::$pass_1);
|
39 |
+
self::$hash_2 = wp_hash_password(self::$pass_2);
|
40 |
+
}
|
41 |
+
|
42 |
+
public function setUp() {
|
43 |
+
parent::setUp();
|
44 |
+
|
45 |
+
$options = self::$lss->options;
|
46 |
+
$options['pw_change_days'] = 10;
|
47 |
+
$options['pw_length'] = 8;
|
48 |
+
$options['pw_reuse_count'] = 3;
|
49 |
+
self::$lss->options = $options;
|
50 |
+
|
51 |
+
self::$lss->set_pw_force_change($this->user->ID);
|
52 |
+
$actual = self::$lss->get_pw_force_change($this->user->ID);
|
53 |
+
$this->assertTrue($actual, 'Problem setting up force change.');
|
54 |
+
|
55 |
+
self::$lss->set_pw_grace_period($this->user->ID);
|
56 |
+
$actual = self::$lss->get_pw_grace_period($this->user->ID);
|
57 |
+
$this->assertGreaterThan(0, $actual, 'Problem setting up grace period.');
|
58 |
+
}
|
59 |
+
|
60 |
+
|
61 |
+
protected function ensure_grace_and_force_are_empty() {
|
62 |
+
$actual = self::$lss->get_pw_force_change($this->user->ID);
|
63 |
+
$this->assertFalse($actual, 'Force change should be false.');
|
64 |
+
|
65 |
+
$actual = self::$lss->get_pw_grace_period($this->user->ID);
|
66 |
+
$this->assertSame(0, $actual, 'Grace period should be 0.');
|
67 |
+
}
|
68 |
+
|
69 |
+
protected function ensure_grace_and_force_are_populated() {
|
70 |
+
$actual = self::$lss->get_pw_force_change($this->user->ID);
|
71 |
+
$this->assertTrue($actual, 'Force change should not be cleared.');
|
72 |
+
|
73 |
+
$actual = self::$lss->get_pw_grace_period($this->user->ID);
|
74 |
+
$this->assertGreaterThan(0, $actual, 'Grace period should not be cleared.');
|
75 |
+
}
|
76 |
+
|
77 |
+
|
78 |
+
/*
|
79 |
+
* HASHES / REUSED
|
80 |
+
*/
|
81 |
+
|
82 |
+
public function test_get_pw_hashes__empty() {
|
83 |
+
global $wpdb;
|
84 |
+
|
85 |
+
$wpdb->query('SAVEPOINT empty');
|
86 |
+
|
87 |
+
$actual = self::$lss->get_pw_hashes($this->user->ID);
|
88 |
+
$this->assertSame(array(), $actual);
|
89 |
+
}
|
90 |
+
|
91 |
+
public function test_save_pw_hash__non_array_edge_case() {
|
92 |
+
update_user_meta($this->user->ID, self::$lss->umk_hashes, 'foo');
|
93 |
+
|
94 |
+
$actual = self::$lss->get_pw_hashes($this->user->ID);
|
95 |
+
$this->assertEquals(array('foo'), $actual);
|
96 |
+
|
97 |
+
delete_user_meta($this->user->ID, self::$lss->umk_hashes);
|
98 |
+
}
|
99 |
+
|
100 |
+
public function test_is_pw_reused__no_reuse_count() {
|
101 |
+
$options = self::$lss->options;
|
102 |
+
$options['pw_reuse_count'] = 0;
|
103 |
+
self::$lss->options = $options;
|
104 |
+
|
105 |
+
$actual = self::$lss->is_pw_reused('abc', $this->user->ID);
|
106 |
+
$this->assertNull($actual);
|
107 |
+
}
|
108 |
+
|
109 |
+
public function test_is_pw_reused__empty() {
|
110 |
+
$actual = self::$lss->is_pw_reused('abc', $this->user->ID);
|
111 |
+
$this->assertSame(0, $actual);
|
112 |
+
}
|
113 |
+
|
114 |
+
public function test_save_pw_hash__new() {
|
115 |
+
$actual = self::$lss->save_pw_hash($this->user->ID, self::$hash_1);
|
116 |
+
$this->assertTrue($actual);
|
117 |
+
}
|
118 |
+
|
119 |
+
public function test_save_pw_hash__exists() {
|
120 |
+
$actual = self::$lss->save_pw_hash($this->user->ID, self::$hash_1);
|
121 |
+
$this->assertSame(1, $actual);
|
122 |
+
}
|
123 |
+
|
124 |
+
public function test_get_pw_hashes__onehash() {
|
125 |
+
$actual = self::$lss->get_pw_hashes($this->user->ID);
|
126 |
+
$this->assertEquals(array(self::$hash_1), $actual);
|
127 |
+
}
|
128 |
+
|
129 |
+
public function test_is_pw_reused__yes() {
|
130 |
+
$actual = self::$lss->is_pw_reused(self::$pass_1, $this->user->ID);
|
131 |
+
$this->assertTrue($actual);
|
132 |
+
}
|
133 |
+
|
134 |
+
public function test_is_pw_reused__no() {
|
135 |
+
$actual = self::$lss->is_pw_reused(self::$pass_2, $this->user->ID);
|
136 |
+
$this->assertFalse($actual);
|
137 |
+
}
|
138 |
+
|
139 |
+
public function test_save_pw_hash__overflow() {
|
140 |
+
global $wpdb;
|
141 |
+
|
142 |
+
self::$lss->save_pw_hash($this->user->ID, 'new1');
|
143 |
+
self::$lss->save_pw_hash($this->user->ID, 'new2');
|
144 |
+
self::$lss->save_pw_hash($this->user->ID, 'new3');
|
145 |
+
|
146 |
+
$expected = array('new1', 'new2', 'new3');
|
147 |
+
$actual = self::$lss->get_pw_hashes($this->user->ID);
|
148 |
+
$this->assertEquals($expected, $actual);
|
149 |
+
|
150 |
+
$wpdb->query('ROLLBACK TO empty');
|
151 |
+
}
|
152 |
+
|
153 |
+
/*
|
154 |
+
* RESET
|
155 |
+
*/
|
156 |
+
|
157 |
+
public function test_password_reset__nullid() {
|
158 |
+
$this->user->ID = null;
|
159 |
+
$actual = self::$lss->password_reset($this->user, self::$pass_2);
|
160 |
+
$this->assertFalse($actual);
|
161 |
+
}
|
162 |
+
|
163 |
+
public function test_password_reset__options_0() {
|
164 |
+
$options = self::$lss->options;
|
165 |
+
$options['pw_change_days'] = 0; // Don't set change time.
|
166 |
+
$options['pw_reuse_count'] = 0; // Don't save hashes.
|
167 |
+
self::$lss->options = $options;
|
168 |
+
|
169 |
+
// Do the deed.
|
170 |
+
$actual = self::$lss->password_reset($this->user, self::$pass_1);
|
171 |
+
$this->assertNull($actual, 'password_reset() should return null.');
|
172 |
+
|
173 |
+
// Check the outcome...
|
174 |
+
$actual = self::$lss->get_pw_changed_time($this->user->ID);
|
175 |
+
$this->assertSame(0, $actual, 'Changed time should be 0.');
|
176 |
+
|
177 |
+
$actual = self::$lss->get_pw_hashes($this->user->ID);
|
178 |
+
$this->assertSame(array(), $actual, 'Hashes should be empty.');
|
179 |
+
|
180 |
+
$this->ensure_grace_and_force_are_empty();
|
181 |
+
}
|
182 |
+
|
183 |
+
public function test_password_reset__normal() {
|
184 |
+
global $wpdb;
|
185 |
+
|
186 |
+
$actual = self::$lss->password_reset($this->user, self::$pass_1);
|
187 |
+
$this->assertNull($actual, 'password_reset() should return null.');
|
188 |
+
|
189 |
+
// Check the outcome.
|
190 |
+
$actual = self::$lss->get_pw_changed_time($this->user->ID);
|
191 |
+
$this->assertGreaterThan(0, $actual, 'Changed time should be 0.');
|
192 |
+
|
193 |
+
$actual = self::$lss->is_pw_reused(self::$pass_1, $this->user->ID);
|
194 |
+
$this->assertTrue($actual, 'Password should show up as reused');
|
195 |
+
|
196 |
+
$this->ensure_grace_and_force_are_empty();
|
197 |
+
|
198 |
+
$wpdb->query('ROLLBACK TO empty');
|
199 |
+
}
|
200 |
+
|
201 |
+
/*
|
202 |
+
* PROFILE UPDATE
|
203 |
+
*/
|
204 |
+
|
205 |
+
/**
|
206 |
+
* @depends test_password_reset__normal
|
207 |
+
*/
|
208 |
+
public function test_profile_update__no_pass() {
|
209 |
+
$errors = new WP_Error;
|
210 |
+
$this->user->user_pass = null;
|
211 |
+
$actual = self::$lss->user_profile_update_errors($errors, 1, $this->user);
|
212 |
+
$this->assertNull($actual);
|
213 |
+
}
|
214 |
+
|
215 |
+
/**
|
216 |
+
* @depends test_password_reset__normal
|
217 |
+
*/
|
218 |
+
public function test_profile_update__update_no_id() {
|
219 |
+
$this->user->ID = null;
|
220 |
+
|
221 |
+
$errors = new WP_Error;
|
222 |
+
$actual = self::$lss->user_profile_update_errors($errors, 1, $this->user);
|
223 |
+
$this->assertNull($actual);
|
224 |
+
}
|
225 |
+
|
226 |
+
/**
|
227 |
+
* @depends test_password_reset__normal
|
228 |
+
*/
|
229 |
+
public function test_profile_update__reused() {
|
230 |
+
global $wpdb;
|
231 |
+
self::$lss->save_pw_hash($this->user->ID, self::$hash_1);
|
232 |
+
|
233 |
+
$errors = new WP_Error;
|
234 |
+
$actual = self::$lss->user_profile_update_errors($errors, 1, $this->user);
|
235 |
+
$this->assertFalse($actual, 'Bad return value.');
|
236 |
+
$this->assertEquals(
|
237 |
+
"<strong>ERROR</strong>: Passwords can not be reused.",
|
238 |
+
$errors->get_error_message()
|
239 |
+
);
|
240 |
+
|
241 |
+
$wpdb->query('ROLLBACK TO empty');
|
242 |
+
}
|
243 |
+
|
244 |
+
/**
|
245 |
+
* @depends test_password_reset__normal
|
246 |
+
*/
|
247 |
+
public function test_profile_update__add_reused_okay() {
|
248 |
+
global $wpdb;
|
249 |
+
self::$lss->save_pw_hash($this->user->ID, self::$hash_1);
|
250 |
+
|
251 |
+
$errors = new WP_Error;
|
252 |
+
$actual = self::$lss->user_profile_update_errors($errors, 0, $this->user);
|
253 |
+
$this->assertTrue($actual, 'Bad return value.');
|
254 |
+
|
255 |
+
$this->ensure_grace_and_force_are_empty();
|
256 |
+
|
257 |
+
$wpdb->query('ROLLBACK TO empty');
|
258 |
+
}
|
259 |
+
|
260 |
+
/**
|
261 |
+
* @depends test_password_reset__normal
|
262 |
+
*/
|
263 |
+
public function test_profile_update__short() {
|
264 |
+
$this->user->user_pass = 'aA1!';
|
265 |
+
|
266 |
+
$errors = new WP_Error;
|
267 |
+
$actual = self::$lss->user_profile_update_errors($errors, 0, $this->user);
|
268 |
+
$this->assertFalse($actual, 'Bad return value.');
|
269 |
+
$this->assertEquals(
|
270 |
+
"<strong>ERROR</strong>: Password is too short.",
|
271 |
+
$errors->get_error_message()
|
272 |
+
);
|
273 |
+
|
274 |
+
$this->ensure_grace_and_force_are_populated();
|
275 |
+
}
|
276 |
+
|
277 |
+
/**
|
278 |
+
* @depends test_password_reset__normal
|
279 |
+
*/
|
280 |
+
public function test_profile_update__add() {
|
281 |
+
$tmp_id = $this->user->ID;
|
282 |
+
$this->user->ID = null;
|
283 |
+
|
284 |
+
$errors = new WP_Error;
|
285 |
+
$actual = self::$lss->user_profile_update_errors($errors, 0, $this->user);
|
286 |
+
$this->assertTrue($actual, 'Bad return value.');
|
287 |
+
|
288 |
+
$this->user->ID = $tmp_id;
|
289 |
+
$this->ensure_grace_and_force_are_populated();
|
290 |
+
}
|
291 |
+
}
|
tests/PasswordExpirationTest.php
ADDED
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Test the password expiration functionality
|
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 |
+
* Test the password expiration functionality
|
19 |
+
*
|
20 |
+
* @package login-security-solution
|
21 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
22 |
+
* @copyright The Analysis and Solutions Company, 2012
|
23 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
24 |
+
*/
|
25 |
+
class PasswordExpirationTest extends TestCase {
|
26 |
+
public static function setUpBeforeClass() {
|
27 |
+
parent::$db_needed = true;
|
28 |
+
parent::set_up_before_class();
|
29 |
+
}
|
30 |
+
|
31 |
+
public function setUp() {
|
32 |
+
parent::setUp();
|
33 |
+
|
34 |
+
$options = self::$lss->options;
|
35 |
+
$options['pw_change_days'] = 10;
|
36 |
+
$options['pw_change_grace_period_minutes'] = 10;
|
37 |
+
self::$lss->options = $options;
|
38 |
+
}
|
39 |
+
|
40 |
+
|
41 |
+
/*
|
42 |
+
* CHANGED TIME CRUD
|
43 |
+
*/
|
44 |
+
|
45 |
+
public function test_get_pw_changed_time__0() {
|
46 |
+
$actual = self::$lss->get_pw_changed_time($this->user->ID);
|
47 |
+
$this->assertSame(0, $actual);
|
48 |
+
}
|
49 |
+
|
50 |
+
public function test_set_pw_changed_time__add() {
|
51 |
+
global $wpdb;
|
52 |
+
$wpdb->query('SAVEPOINT no_pw_change_time');
|
53 |
+
|
54 |
+
$actual = self::$lss->set_pw_changed_time($this->user->ID);
|
55 |
+
$this->assertInternalType('integer', $actual, 'Bad return value.');
|
56 |
+
}
|
57 |
+
|
58 |
+
/**
|
59 |
+
* @depends test_set_pw_changed_time__add
|
60 |
+
*/
|
61 |
+
public function test_set_pw_changed_time__update() {
|
62 |
+
sleep(1);
|
63 |
+
$actual = self::$lss->set_pw_changed_time($this->user->ID);
|
64 |
+
$this->assertTrue($actual, 'Bad return value.');
|
65 |
+
}
|
66 |
+
|
67 |
+
/**
|
68 |
+
* @depends test_set_pw_changed_time__update
|
69 |
+
*/
|
70 |
+
public function test_get_pw_changed_time__something() {
|
71 |
+
global $wpdb;
|
72 |
+
|
73 |
+
$actual = self::$lss->get_pw_changed_time($this->user->ID);
|
74 |
+
$diff = (time() - $actual) < 1;
|
75 |
+
$this->assertGreaterThanOrEqual(0, $diff, 'Time was too long ago.');
|
76 |
+
$this->assertLessThanOrEqual(1, $diff, 'Time was in the future.');
|
77 |
+
|
78 |
+
$wpdb->query('ROLLBACK TO no_pw_change_time');
|
79 |
+
}
|
80 |
+
|
81 |
+
/*
|
82 |
+
* GRACE PERIOD CRUD
|
83 |
+
*/
|
84 |
+
|
85 |
+
public function test_get_pw_grace_period__0() {
|
86 |
+
$actual = self::$lss->get_pw_grace_period($this->user->ID);
|
87 |
+
$this->assertSame(0, $actual);
|
88 |
+
}
|
89 |
+
|
90 |
+
public function test_set_pw_grace_period__add() {
|
91 |
+
$actual = self::$lss->set_pw_grace_period($this->user->ID);
|
92 |
+
$this->assertInternalType('integer', $actual, 'Bad return value.');
|
93 |
+
}
|
94 |
+
|
95 |
+
/**
|
96 |
+
* @depends test_set_pw_grace_period__add
|
97 |
+
*/
|
98 |
+
public function test_set_pw_grace_period__update() {
|
99 |
+
sleep(1);
|
100 |
+
$actual = self::$lss->set_pw_grace_period($this->user->ID);
|
101 |
+
$this->assertTrue($actual, 'Bad return value.');
|
102 |
+
}
|
103 |
+
|
104 |
+
/**
|
105 |
+
* @depends test_set_pw_grace_period__update
|
106 |
+
*/
|
107 |
+
public function test_get_pw_grace_period__something() {
|
108 |
+
$actual = self::$lss->get_pw_grace_period($this->user->ID);
|
109 |
+
$diff = (time() - $actual) < 1;
|
110 |
+
$this->assertGreaterThanOrEqual(0, $diff, 'Time was too long ago.');
|
111 |
+
$this->assertLessThanOrEqual(1, $diff, 'Time was in the future.');
|
112 |
+
}
|
113 |
+
|
114 |
+
/**
|
115 |
+
* @depends test_set_pw_grace_period__add
|
116 |
+
*/
|
117 |
+
public function test_delete_pw_grace_period() {
|
118 |
+
$actual = self::$lss->delete_pw_grace_period($this->user->ID);
|
119 |
+
$this->assertTrue($actual, 'Bad return value.');
|
120 |
+
}
|
121 |
+
|
122 |
+
/*
|
123 |
+
* IS EXPIRED
|
124 |
+
*/
|
125 |
+
|
126 |
+
public function test_is_pw_expired__disabled() {
|
127 |
+
$options = self::$lss->options;
|
128 |
+
$options['pw_change_days'] = 0;
|
129 |
+
self::$lss->options = $options;
|
130 |
+
|
131 |
+
$actual = self::$lss->is_pw_expired($this->user->ID);
|
132 |
+
$this->assertNull($actual, 'Bad return value.');
|
133 |
+
}
|
134 |
+
|
135 |
+
/**
|
136 |
+
* @depends test_set_pw_changed_time__add
|
137 |
+
* @depends test_get_pw_changed_time__something
|
138 |
+
*/
|
139 |
+
public function test_is_pw_expired__new() {
|
140 |
+
$actual = self::$lss->is_pw_expired($this->user->ID);
|
141 |
+
$this->assertSame(0, $actual, 'Bad return value.');
|
142 |
+
}
|
143 |
+
|
144 |
+
/**
|
145 |
+
* @depends test_is_pw_expired__new
|
146 |
+
*/
|
147 |
+
public function test_is_pw_expired__not_expired() {
|
148 |
+
$actual = self::$lss->is_pw_expired($this->user->ID);
|
149 |
+
$this->assertFalse($actual, 'Bad return value.');
|
150 |
+
}
|
151 |
+
|
152 |
+
/**
|
153 |
+
* @depends test_is_pw_expired__new
|
154 |
+
*/
|
155 |
+
public function test_is_pw_expired__expired() {
|
156 |
+
$options = self::$lss->options;
|
157 |
+
$options['pw_change_days'] = -1;
|
158 |
+
self::$lss->options = $options;
|
159 |
+
|
160 |
+
$actual = self::$lss->is_pw_expired($this->user->ID);
|
161 |
+
$this->assertTrue($actual, 'Bad return value.');
|
162 |
+
}
|
163 |
+
|
164 |
+
/*
|
165 |
+
* CHECK GRACE PERIOD
|
166 |
+
*/
|
167 |
+
|
168 |
+
/**
|
169 |
+
* @depends test_delete_pw_grace_period
|
170 |
+
*/
|
171 |
+
public function test_check_pw_grace_period__unset() {
|
172 |
+
$actual = self::$lss->check_pw_grace_period($this->user->ID);
|
173 |
+
$this->assertTrue($actual, 'Bad return value.');
|
174 |
+
}
|
175 |
+
|
176 |
+
/**
|
177 |
+
* @depends test_check_pw_grace_period__unset
|
178 |
+
*/
|
179 |
+
public function test_check_pw_grace_period__in_effect() {
|
180 |
+
$actual = self::$lss->check_pw_grace_period($this->user->ID);
|
181 |
+
$expect = self::$lss->options['pw_change_grace_period_minutes'] * 60;
|
182 |
+
$this->assertSame($expect, $actual, 'Bad return value.');
|
183 |
+
}
|
184 |
+
|
185 |
+
/**
|
186 |
+
* @depends test_check_pw_grace_period__in_effect
|
187 |
+
*/
|
188 |
+
public function test_check_pw_grace_period__expired() {
|
189 |
+
$options = self::$lss->options;
|
190 |
+
$options['pw_change_grace_period_minutes'] = -1;
|
191 |
+
self::$lss->options = $options;
|
192 |
+
|
193 |
+
$actual = self::$lss->check_pw_grace_period($this->user->ID);
|
194 |
+
$this->assertFalse($actual, 'Bad return value.');
|
195 |
+
}
|
196 |
+
|
197 |
+
public function test_delete_pw_grace_period__cleanup() {
|
198 |
+
$actual = self::$lss->delete_pw_grace_period($this->user->ID);
|
199 |
+
$this->assertTrue($actual, 'Bad return value.');
|
200 |
+
}
|
201 |
+
|
202 |
+
/*
|
203 |
+
* CHECK STATUS
|
204 |
+
*/
|
205 |
+
|
206 |
+
public function test_check__empty_user() {
|
207 |
+
$actual = self::$lss->check(array(), null);
|
208 |
+
$this->assertFalse($actual, 'Bad return value.');
|
209 |
+
}
|
210 |
+
|
211 |
+
public function test_check__pre_expiration() {
|
212 |
+
$actual = self::$lss->check(array(), $this->user);
|
213 |
+
$this->assertTrue($actual);
|
214 |
+
}
|
215 |
+
|
216 |
+
/**
|
217 |
+
* @depends test_get_pw_changed_time__something
|
218 |
+
*/
|
219 |
+
public function test_check__post_expiration_first() {
|
220 |
+
$options = self::$lss->options;
|
221 |
+
$options['pw_change_days'] = -1;
|
222 |
+
self::$lss->options = $options;
|
223 |
+
|
224 |
+
$expected_error = 'Cannot modify header information';
|
225 |
+
$this->expected_errors($expected_error);
|
226 |
+
self::$location_expected = get_option('siteurl')
|
227 |
+
. '/wp-login.php?action=login&'
|
228 |
+
. self::$lss->key_login_msg . '=pw_grace';
|
229 |
+
|
230 |
+
$actual = self::$lss->check(array(), $this->user);
|
231 |
+
|
232 |
+
$this->assertTrue($this->were_expected_errors_found(),
|
233 |
+
"Expected error not found: '$expected_error'");
|
234 |
+
$this->assertEquals(self::$location_expected, self::$location_actual,
|
235 |
+
'wp_redirect() produced unexpected location header.');
|
236 |
+
|
237 |
+
$this->assertSame(-1, $actual, 'Bad return value.');
|
238 |
+
}
|
239 |
+
|
240 |
+
/**
|
241 |
+
* @depends test_get_pw_changed_time__something
|
242 |
+
*/
|
243 |
+
public function test_check__post_expiration_grace_expired() {
|
244 |
+
$options = self::$lss->options;
|
245 |
+
$options['pw_change_days'] = -1;
|
246 |
+
$options['pw_change_grace_period_minutes'] = -1;
|
247 |
+
self::$lss->options = $options;
|
248 |
+
|
249 |
+
$expected_error = 'Cannot modify header information';
|
250 |
+
$this->expected_errors($expected_error);
|
251 |
+
self::$location_expected = get_option('siteurl')
|
252 |
+
. '/wp-login.php?action=retrievepassword&'
|
253 |
+
. self::$lss->key_login_msg . '=pw_expired';
|
254 |
+
|
255 |
+
$actual = self::$lss->check(array(), $this->user);
|
256 |
+
|
257 |
+
$this->assertTrue($this->were_expected_errors_found(),
|
258 |
+
"Expected error not found: '$expected_error'");
|
259 |
+
$this->assertEquals(self::$location_expected, self::$location_actual,
|
260 |
+
'wp_redirect() produced unexpected location header.');
|
261 |
+
|
262 |
+
$this->assertSame(-2, $actual, 'Bad return value.');
|
263 |
+
}
|
264 |
+
|
265 |
+
public function test_redirect_to_login__other() {
|
266 |
+
$_SERVER['REQUEST_URI'] = '/some/page';
|
267 |
+
|
268 |
+
$expected_error = 'Cannot modify header information';
|
269 |
+
$this->expected_errors($expected_error);
|
270 |
+
self::$location_expected = get_option('siteurl')
|
271 |
+
. '/wp-login.php?redirect_to=%2Fsome%2Fpage&action=acti%26n&'
|
272 |
+
. self::$lss->key_login_msg . '=me%26g';
|
273 |
+
|
274 |
+
self::$lss->redirect_to_login('me&g', true, 'acti&n');
|
275 |
+
|
276 |
+
$this->assertTrue($this->were_expected_errors_found(),
|
277 |
+
"Expected error not found: '$expected_error'");
|
278 |
+
$this->assertEquals(self::$location_expected, self::$location_actual,
|
279 |
+
'wp_redirect() produced unexpected location header.');
|
280 |
+
}
|
281 |
+
}
|
tests/PasswordForceChangeTest.php
ADDED
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Test the force password change functionality
|
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 |
+
* Test the force password change functionality
|
19 |
+
*
|
20 |
+
* @package login-security-solution
|
21 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
22 |
+
* @copyright The Analysis and Solutions Company, 2012
|
23 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
24 |
+
*/
|
25 |
+
class PasswordForceChangeTest extends TestCase {
|
26 |
+
public static function setUpBeforeClass() {
|
27 |
+
parent::$db_needed = true;
|
28 |
+
parent::set_up_before_class();
|
29 |
+
}
|
30 |
+
|
31 |
+
|
32 |
+
public function test_get_pw_force_change__false_1() {
|
33 |
+
$actual = self::$lss->get_pw_force_change($this->user->ID);
|
34 |
+
$this->assertFalse($actual);
|
35 |
+
}
|
36 |
+
|
37 |
+
public function test_check__pre_threshold() {
|
38 |
+
$actual = self::$lss->check(array(), $this->user);
|
39 |
+
$this->assertTrue($actual);
|
40 |
+
}
|
41 |
+
|
42 |
+
public function test_set_pw_force_change__add() {
|
43 |
+
$actual = self::$lss->set_pw_force_change($this->user->ID);
|
44 |
+
$this->assertInternalType('integer', $actual, 'Bad return value.');
|
45 |
+
}
|
46 |
+
|
47 |
+
/**
|
48 |
+
* @depends test_set_pw_force_change__add
|
49 |
+
*/
|
50 |
+
public function test_get_pw_force_change__true() {
|
51 |
+
$actual = self::$lss->get_pw_force_change($this->user->ID);
|
52 |
+
$this->assertTrue($actual);
|
53 |
+
}
|
54 |
+
|
55 |
+
/**
|
56 |
+
* @depends test_get_pw_force_change__true
|
57 |
+
*/
|
58 |
+
public function test_check__post_threshold() {
|
59 |
+
$expected_error = 'Cannot modify header information';
|
60 |
+
$this->expected_errors($expected_error);
|
61 |
+
self::$location_expected = get_option('siteurl')
|
62 |
+
. '/wp-login.php?action=retrievepassword&'
|
63 |
+
. self::$lss->key_login_msg . '=pw_force';
|
64 |
+
|
65 |
+
$actual = self::$lss->check(array(), $this->user);
|
66 |
+
|
67 |
+
$this->assertTrue($this->were_expected_errors_found(),
|
68 |
+
"Expected error not found: '$expected_error'");
|
69 |
+
$this->assertEquals(self::$location_expected, self::$location_actual,
|
70 |
+
'wp_redirect() produced unexpected location header.');
|
71 |
+
|
72 |
+
$this->assertSame(-3, $actual, 'Bad return value.');
|
73 |
+
}
|
74 |
+
|
75 |
+
/**
|
76 |
+
* @depends test_get_pw_force_change__true
|
77 |
+
*/
|
78 |
+
public function test_delete_pw_force_change() {
|
79 |
+
$actual = self::$lss->delete_pw_force_change($this->user->ID);
|
80 |
+
$this->assertTrue($actual);
|
81 |
+
}
|
82 |
+
|
83 |
+
/**
|
84 |
+
* @depends test_delete_pw_force_change
|
85 |
+
*/
|
86 |
+
public function test_get_pw_force_change__false_2() {
|
87 |
+
$actual = self::$lss->get_pw_force_change($this->user->ID);
|
88 |
+
$this->assertFalse($actual);
|
89 |
+
}
|
90 |
+
}
|
tests/PasswordValidationTest.php
ADDED
@@ -0,0 +1,660 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Test the password validation functionality
|
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 |
+
* Test the password validation functionality
|
19 |
+
*
|
20 |
+
* @package login-security-solution
|
21 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
22 |
+
* @copyright The Analysis and Solutions Company, 2012
|
23 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
24 |
+
*/
|
25 |
+
class PasswordValidationTest extends TestCase {
|
26 |
+
protected static $dict_available;
|
27 |
+
protected static $grep_available;
|
28 |
+
protected static $mbstring_available;
|
29 |
+
|
30 |
+
|
31 |
+
public static function setUpBeforeClass() {
|
32 |
+
parent::$db_needed = false;
|
33 |
+
parent::set_up_before_class();
|
34 |
+
|
35 |
+
if (self::$lss->is_pw_dict_program('zygote')) {
|
36 |
+
self::$dict_available = true;
|
37 |
+
}
|
38 |
+
if (self::$lss->is_pw_dictionary__grep('Pa$$w0rd1')) {
|
39 |
+
self::$grep_available = true;
|
40 |
+
}
|
41 |
+
self::$mbstring_available = extension_loaded('mbstring');
|
42 |
+
}
|
43 |
+
|
44 |
+
public function setUp() {
|
45 |
+
parent::setUp();
|
46 |
+
self::$lss->available_mbstring = extension_loaded('mbstring');
|
47 |
+
|
48 |
+
$options = self::$lss->options;
|
49 |
+
$options['pw_complexity_exemption_length'] = 20;
|
50 |
+
$options['pw_length'] = 8;
|
51 |
+
self::$lss->options = $options;
|
52 |
+
|
53 |
+
self::$lss->available_dict = self::$dict_available;
|
54 |
+
self::$lss->available_grep = self::$grep_available;
|
55 |
+
self::$lss->available_mbstring = self::$mbstring_available;
|
56 |
+
}
|
57 |
+
|
58 |
+
|
59 |
+
public function test_is_pw_dictionary__grepavail() {
|
60 |
+
if (!self::$dict_available) {
|
61 |
+
$this->markTestSkipped('grep not available');
|
62 |
+
}
|
63 |
+
self::$lss->available_grep = true;
|
64 |
+
$actual = self::$lss->is_pw_dictionary('Pa$$w0rd1');
|
65 |
+
$this->assertTrue($actual);
|
66 |
+
}
|
67 |
+
|
68 |
+
public function test_is_pw_dictionary__grepunavail() {
|
69 |
+
self::$lss->available_grep = false;
|
70 |
+
$actual = self::$lss->is_pw_dictionary('Pa$$w0rd1');
|
71 |
+
$this->assertTrue($actual);
|
72 |
+
}
|
73 |
+
|
74 |
+
|
75 |
+
public function test_dict_program__unavailable() {
|
76 |
+
self::$lss->available_dict = false;
|
77 |
+
$actual = self::$lss->is_pw_dict_program('foo');
|
78 |
+
$this->assertNull($actual);
|
79 |
+
}
|
80 |
+
|
81 |
+
public function test_dict_program_false() {
|
82 |
+
if (!self::$dict_available) {
|
83 |
+
$this->markTestSkipped('dict is not available');
|
84 |
+
}
|
85 |
+
$tests = array(
|
86 |
+
"thiscannotbeaword",
|
87 |
+
"简化字的昨天今天和明天",
|
88 |
+
);
|
89 |
+
foreach ($tests as $pw) {
|
90 |
+
$actual = self::$lss->is_pw_dict_program($pw);
|
91 |
+
$this->assertFalse($actual, "Should have passed: '$pw'");
|
92 |
+
}
|
93 |
+
}
|
94 |
+
public function test_dict_program_true() {
|
95 |
+
if (!self::$dict_available) {
|
96 |
+
$this->markTestSkipped('dict is not available');
|
97 |
+
}
|
98 |
+
$tests = array(
|
99 |
+
"password",
|
100 |
+
);
|
101 |
+
foreach ($tests as $pw) {
|
102 |
+
$actual = self::$lss->is_pw_dict_program($pw);
|
103 |
+
$this->assertTrue($actual, "Should have failed: '$pw'");
|
104 |
+
}
|
105 |
+
}
|
106 |
+
|
107 |
+
public function test_dictionary__grep_unavailable() {
|
108 |
+
self::$lss->available_grep = false;
|
109 |
+
$actual = self::$lss->is_pw_dictionary__grep('foo');
|
110 |
+
$this->assertNull($actual);
|
111 |
+
}
|
112 |
+
|
113 |
+
public function test_dictionary__file_false() {
|
114 |
+
if (!self::$grep_available) {
|
115 |
+
$this->markTestSkipped('grep is not available');
|
116 |
+
}
|
117 |
+
$tests = array(
|
118 |
+
"thiscannotbeaword",
|
119 |
+
"化字的昨天今天和明",
|
120 |
+
);
|
121 |
+
foreach ($tests as $pw) {
|
122 |
+
$actual = self::$lss->is_pw_dictionary__file($pw);
|
123 |
+
$this->assertFalse($actual, "Should have passed: '$pw'");
|
124 |
+
}
|
125 |
+
}
|
126 |
+
public function test_dictionary__file_true() {
|
127 |
+
if (!self::$grep_available) {
|
128 |
+
$this->markTestSkipped('grep is not available');
|
129 |
+
}
|
130 |
+
$tests = array(
|
131 |
+
'Pa$$w0rd1',
|
132 |
+
"简化字的昨天今天和明天",
|
133 |
+
);
|
134 |
+
foreach ($tests as $pw) {
|
135 |
+
$actual = self::$lss->is_pw_dictionary__file($pw);
|
136 |
+
$this->assertTrue($actual, "Should have failed: '$pw'");
|
137 |
+
}
|
138 |
+
}
|
139 |
+
|
140 |
+
public function test_dictionary__grep_false() {
|
141 |
+
if (!self::$grep_available) {
|
142 |
+
$this->markTestSkipped('grep is not available');
|
143 |
+
}
|
144 |
+
$tests = array(
|
145 |
+
"thiscannotbeaword",
|
146 |
+
"化字的昨天今天和明",
|
147 |
+
);
|
148 |
+
foreach ($tests as $pw) {
|
149 |
+
$actual = self::$lss->is_pw_dictionary__grep($pw);
|
150 |
+
$this->assertFalse($actual, "Should have passed: '$pw'");
|
151 |
+
}
|
152 |
+
}
|
153 |
+
public function test_dictionary__grep_true() {
|
154 |
+
if (!self::$grep_available) {
|
155 |
+
$this->markTestSkipped('grep is not available');
|
156 |
+
}
|
157 |
+
$tests = array(
|
158 |
+
'Pa$$w0rd1',
|
159 |
+
"简化字的昨天今天和明天",
|
160 |
+
);
|
161 |
+
foreach ($tests as $pw) {
|
162 |
+
$actual = self::$lss->is_pw_dictionary__grep($pw);
|
163 |
+
$this->assertTrue($actual, "Should have failed: '$pw'");
|
164 |
+
}
|
165 |
+
}
|
166 |
+
|
167 |
+
public function test_like_bloginfo_false() {
|
168 |
+
$tests = array(
|
169 |
+
"zzzzzzzzz",
|
170 |
+
"简化字的昨天今天和明天",
|
171 |
+
);
|
172 |
+
foreach ($tests as $pw) {
|
173 |
+
$actual = self::$lss->is_pw_like_bloginfo($pw);
|
174 |
+
$this->assertFalse($actual, "Should have passed: '$pw'");
|
175 |
+
}
|
176 |
+
}
|
177 |
+
public function test_like_bloginfo_true() {
|
178 |
+
$tests = array(
|
179 |
+
get_bloginfo('name'),
|
180 |
+
'othertextwith' . get_bloginfo('name'),
|
181 |
+
get_bloginfo('url'),
|
182 |
+
'othertextwith' . get_bloginfo('url'),
|
183 |
+
get_bloginfo('description'),
|
184 |
+
'othertextwith' . get_bloginfo('description'),
|
185 |
+
);
|
186 |
+
foreach ($tests as $pw) {
|
187 |
+
$actual = self::$lss->is_pw_like_bloginfo($pw);
|
188 |
+
$this->assertTrue($actual, "Should have failed: '$pw'");
|
189 |
+
}
|
190 |
+
}
|
191 |
+
|
192 |
+
public function test_like_user_data_false() {
|
193 |
+
$tests = array(
|
194 |
+
"yyyy",
|
195 |
+
"简字昨今和天",
|
196 |
+
);
|
197 |
+
foreach ($tests as $pw) {
|
198 |
+
$actual = self::$lss->is_pw_like_user_data($pw, $this->user);
|
199 |
+
$this->assertFalse($actual, "Should have passed: '$pw'");
|
200 |
+
}
|
201 |
+
}
|
202 |
+
public function test_like_user_data_true() {
|
203 |
+
$tests = array(
|
204 |
+
"aaaa",
|
205 |
+
"bbbbbbbb",
|
206 |
+
"cccccccc",
|
207 |
+
"dddddddd",
|
208 |
+
"eeeeeeee",
|
209 |
+
"ffff",
|
210 |
+
"简化字的昨天今天和明天",
|
211 |
+
"hhhh",
|
212 |
+
"iiii",
|
213 |
+
"jjjj",
|
214 |
+
);
|
215 |
+
foreach ($tests as $pw) {
|
216 |
+
$actual = self::$lss->is_pw_like_user_data($pw, $this->user);
|
217 |
+
$this->assertTrue($actual, "Should have failed: '$pw'");
|
218 |
+
}
|
219 |
+
}
|
220 |
+
|
221 |
+
public function test_outside_ascii_false() {
|
222 |
+
$tests = array(
|
223 |
+
"aba!123",
|
224 |
+
"aba~123",
|
225 |
+
);
|
226 |
+
foreach ($tests as $pw) {
|
227 |
+
$actual = self::$lss->is_pw_outside_ascii($pw);
|
228 |
+
$this->assertFalse($actual, "Should have passed: '$pw'");
|
229 |
+
}
|
230 |
+
}
|
231 |
+
public function test_outside_ascii_true() {
|
232 |
+
$tests = array(
|
233 |
+
"aba\n123",
|
234 |
+
"简化字的昨天今天和明天",
|
235 |
+
);
|
236 |
+
foreach ($tests as $pw) {
|
237 |
+
$actual = self::$lss->is_pw_outside_ascii($pw);
|
238 |
+
$this->assertTrue($actual, "Should have failed: '$pw'");
|
239 |
+
}
|
240 |
+
}
|
241 |
+
|
242 |
+
public function test_missing_numeric_false() {
|
243 |
+
$tests = array(
|
244 |
+
"aA1!",
|
245 |
+
"123",
|
246 |
+
"ةيب8رعلا",
|
247 |
+
);
|
248 |
+
foreach ($tests as $pw) {
|
249 |
+
$actual = self::$lss->is_pw_missing_numeric($pw);
|
250 |
+
$this->assertFalse($actual, "Should have passed: '$pw'");
|
251 |
+
}
|
252 |
+
}
|
253 |
+
public function test_missing_numeric_true() {
|
254 |
+
$tests = array(
|
255 |
+
"abc",
|
256 |
+
"ABC",
|
257 |
+
"!@#",
|
258 |
+
"aAb",
|
259 |
+
"a!a!",
|
260 |
+
"A!A!",
|
261 |
+
"aA!",
|
262 |
+
"ةيبرعلا",
|
263 |
+
);
|
264 |
+
foreach ($tests as $pw) {
|
265 |
+
$actual = self::$lss->is_pw_missing_numeric($pw);
|
266 |
+
$this->assertTrue($actual, "Should have failed: '$pw'");
|
267 |
+
}
|
268 |
+
}
|
269 |
+
|
270 |
+
public function test_missing_punct_chars_false() {
|
271 |
+
$tests = array(
|
272 |
+
"aA1!",
|
273 |
+
"#",
|
274 |
+
".",
|
275 |
+
"Россия.",
|
276 |
+
"「中國哲學史大綱」、胡適",
|
277 |
+
);
|
278 |
+
foreach ($tests as $pw) {
|
279 |
+
$actual = self::$lss->is_pw_missing_punct_chars($pw);
|
280 |
+
$this->assertFalse($actual, "Should have passed: '$pw'");
|
281 |
+
}
|
282 |
+
}
|
283 |
+
public function test_missing_punct_chars_true() {
|
284 |
+
$tests = array(
|
285 |
+
"123",
|
286 |
+
"abc",
|
287 |
+
"ABC",
|
288 |
+
"aAb",
|
289 |
+
"a1a",
|
290 |
+
"aA1",
|
291 |
+
"a",
|
292 |
+
"A",
|
293 |
+
"1",
|
294 |
+
"Россия",
|
295 |
+
"中國哲學史大綱胡適",
|
296 |
+
);
|
297 |
+
foreach ($tests as $pw) {
|
298 |
+
$actual = self::$lss->is_pw_missing_punct_chars($pw);
|
299 |
+
$this->assertTrue($actual, "Should have failed: '$pw'");
|
300 |
+
}
|
301 |
+
}
|
302 |
+
|
303 |
+
public function test_missing_upper_lower_chars_false() {
|
304 |
+
$tests = array(
|
305 |
+
"aA1!",
|
306 |
+
"aAb",
|
307 |
+
"aA!",
|
308 |
+
"БбƤƥ", // Bicameral UTF-8.
|
309 |
+
"חح", // Unicameral UTF-8.
|
310 |
+
);
|
311 |
+
foreach ($tests as $pw) {
|
312 |
+
$actual = self::$lss->is_pw_missing_upper_lower_chars($pw);
|
313 |
+
$this->assertFalse($actual, "Should have passed: '$pw'");
|
314 |
+
}
|
315 |
+
}
|
316 |
+
public function test_missing_upper_lower_chars_true() {
|
317 |
+
$tests = array(
|
318 |
+
"123",
|
319 |
+
"abc",
|
320 |
+
"ABC",
|
321 |
+
"!@#",
|
322 |
+
"a1a",
|
323 |
+
"a!a!",
|
324 |
+
"A!A!",
|
325 |
+
"a1!",
|
326 |
+
"A1!",
|
327 |
+
"бƥ",
|
328 |
+
"БƤ",
|
329 |
+
);
|
330 |
+
foreach ($tests as $pw) {
|
331 |
+
$actual = self::$lss->is_pw_missing_upper_lower_chars($pw);
|
332 |
+
$this->assertTrue($actual, "Should have failed: '$pw'");
|
333 |
+
}
|
334 |
+
}
|
335 |
+
|
336 |
+
public function test_missing_upper_lower_chars_false__nomb() {
|
337 |
+
self::$lss->available_mbstring = false;
|
338 |
+
$tests = array(
|
339 |
+
"aA1!",
|
340 |
+
"aAb",
|
341 |
+
"aA!",
|
342 |
+
);
|
343 |
+
foreach ($tests as $pw) {
|
344 |
+
$actual = self::$lss->is_pw_missing_upper_lower_chars($pw);
|
345 |
+
$this->assertFalse($actual, "Should have passed: '$pw'");
|
346 |
+
}
|
347 |
+
}
|
348 |
+
|
349 |
+
public function test_missing_upper_lower_chars_true__nomb() {
|
350 |
+
self::$lss->available_mbstring = false;
|
351 |
+
$tests = array(
|
352 |
+
"123",
|
353 |
+
"abc",
|
354 |
+
"ABC",
|
355 |
+
"!@#",
|
356 |
+
"a1a",
|
357 |
+
"a!a!",
|
358 |
+
"A!A!",
|
359 |
+
"a1!",
|
360 |
+
"A1!",
|
361 |
+
);
|
362 |
+
foreach ($tests as $pw) {
|
363 |
+
$actual = self::$lss->is_pw_missing_upper_lower_chars($pw);
|
364 |
+
$this->assertTrue($actual, "Should have failed: '$pw'");
|
365 |
+
}
|
366 |
+
}
|
367 |
+
|
368 |
+
public function test_sequential_codepoints_false() {
|
369 |
+
$tests = array(
|
370 |
+
"agke58#",
|
371 |
+
"תירִבְעִ",
|
372 |
+
"ⲘⲉⲧⲢⲉⲙ̀ⲛⲭⲏⲙⲓ",
|
373 |
+
);
|
374 |
+
foreach ($tests as $pw) {
|
375 |
+
$actual = self::$lss->is_pw_sequential_codepoints($pw);
|
376 |
+
$this->assertFalse($actual, "Should have passed: '$pw'");
|
377 |
+
}
|
378 |
+
}
|
379 |
+
public function test_sequential_codepoints_true() {
|
380 |
+
$tests = array(
|
381 |
+
"1234",
|
382 |
+
"abcd",
|
383 |
+
"ABCD",
|
384 |
+
"%&'(",
|
385 |
+
"דגבא",
|
386 |
+
"ϣϥϧ",
|
387 |
+
);
|
388 |
+
foreach ($tests as $pw) {
|
389 |
+
$actual = self::$lss->is_pw_sequential_codepoints($pw);
|
390 |
+
$this->assertTrue($actual, "Should have failed: '$pw'");
|
391 |
+
}
|
392 |
+
}
|
393 |
+
|
394 |
+
public function test_sequential_file_false() {
|
395 |
+
$tests = array(
|
396 |
+
"1357",
|
397 |
+
"adg!#%135yip579", // not sequential
|
398 |
+
"adg!#%135yu579", // "yu" is sequential, but too short
|
399 |
+
);
|
400 |
+
foreach ($tests as $pw) {
|
401 |
+
$actual = self::$lss->is_pw_sequential_file($pw);
|
402 |
+
$this->assertFalse($actual, "Should have passed: '$pw'");
|
403 |
+
}
|
404 |
+
}
|
405 |
+
public function test_sequential_file_true() {
|
406 |
+
$tests = array(
|
407 |
+
"^&*()",
|
408 |
+
"asdf",
|
409 |
+
"QWERT",
|
410 |
+
"fdsa",
|
411 |
+
"adg!#%135yui579", // "yui" is sequential
|
412 |
+
"adg!#%135iuy579", // "iuy" is "yui" sequence reversed
|
413 |
+
);
|
414 |
+
foreach ($tests as $pw) {
|
415 |
+
$actual = self::$lss->is_pw_sequential_file($pw);
|
416 |
+
$this->assertTrue($actual, "Should have failed: '$pw'");
|
417 |
+
}
|
418 |
+
}
|
419 |
+
|
420 |
+
public function test_strip_nonword_chars() {
|
421 |
+
$tests = array(
|
422 |
+
"^a&*b()c2" => "abc2",
|
423 |
+
"「中國哲學史大綱」、胡適" => "中國哲學史大綱胡適",
|
424 |
+
"2חַ״וּדּ" => "2חוד",
|
425 |
+
);
|
426 |
+
foreach ($tests as $in => $expect) {
|
427 |
+
$actual = self::$lss->strip_nonword_chars($in);
|
428 |
+
$this->assertEquals($expect, $actual);
|
429 |
+
}
|
430 |
+
}
|
431 |
+
|
432 |
+
|
433 |
+
public function test_validate_pw__notset() {
|
434 |
+
$errors = new WP_Error;
|
435 |
+
$user = new stdClass;
|
436 |
+
$actual = self::$lss->validate_pw($user, $errors);
|
437 |
+
$this->assertFalse($actual,
|
438 |
+
"password not being set should have failed.");
|
439 |
+
$this->assertEquals(
|
440 |
+
__("<strong>ERROR</strong>: Password not set.", self::ID),
|
441 |
+
$errors->get_error_message()
|
442 |
+
);
|
443 |
+
}
|
444 |
+
|
445 |
+
public function test_validate_pw__array() {
|
446 |
+
$errors = new WP_Error;
|
447 |
+
$actual = self::$lss->validate_pw(array('abc'), $errors);
|
448 |
+
$this->assertFalse($actual,
|
449 |
+
"'array('abc')' should have failed.");
|
450 |
+
$this->assertEquals(
|
451 |
+
__("<strong>ERROR</strong>: Passwords must be strings.", self::ID),
|
452 |
+
$errors->get_error_message()
|
453 |
+
);
|
454 |
+
}
|
455 |
+
|
456 |
+
public function test_validate_pw__ascii() {
|
457 |
+
self::$lss->available_mbstring = false;
|
458 |
+
|
459 |
+
$errors = new WP_Error;
|
460 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
461 |
+
$this->assertFalse($actual,
|
462 |
+
"'" . $this->user->user_pass . "' should have failed.");
|
463 |
+
$this->assertEquals(
|
464 |
+
__("<strong>ERROR</strong>: Passwords must use ASCII characters.", self::ID),
|
465 |
+
$errors->get_error_message()
|
466 |
+
);
|
467 |
+
}
|
468 |
+
|
469 |
+
public function test_validate_pw__short_mb() {
|
470 |
+
if (!self::$mbstring_available) {
|
471 |
+
$this->markTestSkipped('mbstring not available');
|
472 |
+
}
|
473 |
+
|
474 |
+
$this->user->user_pass = '简化字的昨天今';
|
475 |
+
|
476 |
+
$errors = new WP_Error;
|
477 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
478 |
+
$this->assertFalse($actual,
|
479 |
+
"'" . $this->user->user_pass . "' should have failed.");
|
480 |
+
$this->assertEquals(
|
481 |
+
__("<strong>ERROR</strong>: Password is too short.", self::ID),
|
482 |
+
$errors->get_error_message()
|
483 |
+
);
|
484 |
+
}
|
485 |
+
|
486 |
+
public function test_validate_pw__short_nomb() {
|
487 |
+
self::$lss->available_mbstring = false;
|
488 |
+
$this->user->user_pass = 'aA1!';
|
489 |
+
|
490 |
+
$errors = new WP_Error;
|
491 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
492 |
+
$this->assertFalse($actual,
|
493 |
+
"'" . $this->user->user_pass . "' should have failed.");
|
494 |
+
$this->assertEquals(
|
495 |
+
__("<strong>ERROR</strong>: Password is too short.", self::ID),
|
496 |
+
$errors->get_error_message()
|
497 |
+
);
|
498 |
+
}
|
499 |
+
|
500 |
+
public function test_validate_pw__nopunct() {
|
501 |
+
$this->user->user_pass = '123456789012';
|
502 |
+
|
503 |
+
$errors = new WP_Error;
|
504 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
505 |
+
$this->assertFalse($actual,
|
506 |
+
"'" . $this->user->user_pass . "' should have failed.");
|
507 |
+
$this->assertEquals(
|
508 |
+
sprintf(__("<strong>ERROR</strong>: Passwords must either contain punctuation marks / symbols or be %d characters long.", self::ID), self::$lss->options['pw_complexity_exemption_length']),
|
509 |
+
$errors->get_error_message()
|
510 |
+
);
|
511 |
+
}
|
512 |
+
|
513 |
+
public function test_validate_pw__nonumbers() {
|
514 |
+
$this->user->user_pass = 'axj*@UXyqy';
|
515 |
+
|
516 |
+
$errors = new WP_Error;
|
517 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
518 |
+
$this->assertFalse($actual,
|
519 |
+
"'" . $this->user->user_pass . "' should have failed.");
|
520 |
+
$this->assertEquals(
|
521 |
+
sprintf(__("<strong>ERROR</strong>: Passwords must either contain numbers or be %d characters long.", self::ID), self::$lss->options['pw_complexity_exemption_length']),
|
522 |
+
$errors->get_error_message()
|
523 |
+
);
|
524 |
+
}
|
525 |
+
|
526 |
+
public function test_validate_pw__noupperlower() {
|
527 |
+
$this->user->user_pass = 'axj*1@yqy';
|
528 |
+
|
529 |
+
$errors = new WP_Error;
|
530 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
531 |
+
$this->assertFalse($actual,
|
532 |
+
"'" . $this->user->user_pass . "' should have failed.");
|
533 |
+
$this->assertEquals(
|
534 |
+
sprintf(__("<strong>ERROR</strong>: Passwords must either contain upper-case and lower-case letters or be %d characters long.", self::ID), self::$lss->options['pw_complexity_exemption_length']),
|
535 |
+
$errors->get_error_message()
|
536 |
+
);
|
537 |
+
}
|
538 |
+
|
539 |
+
public function test_validate_pw__sequentialfile() {
|
540 |
+
$this->user->user_pass = 'alGb02i&*()';
|
541 |
+
|
542 |
+
$errors = new WP_Error;
|
543 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
544 |
+
$this->assertFalse($actual,
|
545 |
+
"'" . $this->user->user_pass . "' should have failed.");
|
546 |
+
$this->assertEquals(
|
547 |
+
__("<strong>ERROR</strong>: Passwords can't be sequential keys.", self::ID),
|
548 |
+
$errors->get_error_message()
|
549 |
+
);
|
550 |
+
}
|
551 |
+
|
552 |
+
public function test_validate_pw__sequentialcodepoints() {
|
553 |
+
$this->user->user_pass = 'abAB12!@';
|
554 |
+
|
555 |
+
$errors = new WP_Error;
|
556 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
557 |
+
$this->assertFalse($actual,
|
558 |
+
"'" . $this->user->user_pass . "' should have failed.");
|
559 |
+
$this->assertEquals(
|
560 |
+
__("<strong>ERROR</strong>: Passwords can't have that many sequential characters.", self::ID),
|
561 |
+
$errors->get_error_message()
|
562 |
+
);
|
563 |
+
}
|
564 |
+
|
565 |
+
public function test_validate_pw__userdata() {
|
566 |
+
$this->user->user_pass = $this->user->nickname;
|
567 |
+
|
568 |
+
$errors = new WP_Error;
|
569 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
570 |
+
$this->assertFalse($actual,
|
571 |
+
"'" . $this->user->user_pass . "' should have failed.");
|
572 |
+
$this->assertEquals(
|
573 |
+
__("<strong>ERROR</strong>: Passwords can't contain user data.", self::ID),
|
574 |
+
$errors->get_error_message()
|
575 |
+
);
|
576 |
+
}
|
577 |
+
|
578 |
+
public function test_validate_pw__userdata_leet() {
|
579 |
+
$this->user->user_pass = 'this@@@@ShouldGetNa1led';
|
580 |
+
|
581 |
+
$errors = new WP_Error;
|
582 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
583 |
+
$this->assertFalse($actual,
|
584 |
+
"'" . $this->user->user_pass . "' should have failed.");
|
585 |
+
$this->assertEquals(
|
586 |
+
__("<strong>ERROR</strong>: Passwords can't contain user data.", self::ID),
|
587 |
+
$errors->get_error_message()
|
588 |
+
);
|
589 |
+
}
|
590 |
+
|
591 |
+
public function test_validate_pw__bloginfo() {
|
592 |
+
$this->user->user_pass = 'Ja!k2' . get_bloginfo('description');
|
593 |
+
|
594 |
+
$errors = new WP_Error;
|
595 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
596 |
+
$this->assertFalse($actual,
|
597 |
+
"'" . $this->user->user_pass . "' should have failed.");
|
598 |
+
$this->assertEquals(
|
599 |
+
__("<strong>ERROR</strong>: Passwords can't contain site info.", self::ID),
|
600 |
+
$errors->get_error_message()
|
601 |
+
);
|
602 |
+
}
|
603 |
+
|
604 |
+
public function test_validate_pw__dictionary() {
|
605 |
+
$this->user->user_pass = 'Pa$$w0rd1';
|
606 |
+
|
607 |
+
$errors = new WP_Error;
|
608 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
609 |
+
$this->assertFalse($actual,
|
610 |
+
"'" . $this->user->user_pass . "' should have failed.");
|
611 |
+
$this->assertEquals(
|
612 |
+
__("<strong>ERROR</strong>: Password is too common.", self::ID),
|
613 |
+
$errors->get_error_message()
|
614 |
+
);
|
615 |
+
}
|
616 |
+
|
617 |
+
public function test_validate_pw__dict() {
|
618 |
+
if (!self::$dict_available) {
|
619 |
+
$this->markTestSkipped('grep not available');
|
620 |
+
}
|
621 |
+
$this->user->user_pass = 'R3n0vat!on';
|
622 |
+
|
623 |
+
$errors = new WP_Error;
|
624 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
625 |
+
$this->assertFalse($actual,
|
626 |
+
"'" . $this->user->user_pass . "' should have failed.");
|
627 |
+
$this->assertEquals(
|
628 |
+
__("<strong>ERROR</strong>: Passwords can't be variations of dictionary words.", self::ID),
|
629 |
+
$errors->get_error_message()
|
630 |
+
);
|
631 |
+
}
|
632 |
+
|
633 |
+
public function test_validate_pw__good() {
|
634 |
+
$errors = new WP_Error;
|
635 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
636 |
+
$this->assertTrue($actual,
|
637 |
+
"'" . $this->user->user_pass . "' should have passed.");
|
638 |
+
$this->assertEmpty($errors->get_error_message());
|
639 |
+
}
|
640 |
+
|
641 |
+
public function test_validate_pw__good_complex_exempt() {
|
642 |
+
$this->user->user_pass = 'this is a very long password not complex';
|
643 |
+
|
644 |
+
$errors = new WP_Error;
|
645 |
+
$actual = self::$lss->validate_pw($this->user, $errors);
|
646 |
+
$this->assertTrue($actual,
|
647 |
+
"'" . $this->user->user_pass . "' should have passed.");
|
648 |
+
$this->assertEmpty($errors->get_error_message());
|
649 |
+
}
|
650 |
+
|
651 |
+
public function test_has_match_array() {
|
652 |
+
$actual = self::$lss->has_match('foo', array());
|
653 |
+
$this->assertFalse($actual);
|
654 |
+
}
|
655 |
+
|
656 |
+
public function test_has_match_empty() {
|
657 |
+
$actual = self::$lss->has_match('foo', '');
|
658 |
+
$this->assertFalse($actual);
|
659 |
+
}
|
660 |
+
}
|
tests/TestCase.php
ADDED
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Parent TestCase class containing common methods and properties, plus an
|
5 |
+
* override for the wp_mail() function
|
6 |
+
*
|
7 |
+
* @package login-security-solution
|
8 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
9 |
+
* @copyright The Analysis and Solutions Company, 2012
|
10 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
11 |
+
*/
|
12 |
+
|
13 |
+
/*
|
14 |
+
* Keep PHPUnit from messing up WordPress' crazy use of globals.
|
15 |
+
*
|
16 |
+
* This prevents the following errors:
|
17 |
+
* Call to a member function add_rule() on a non-object in wp-includes/rewrite.php
|
18 |
+
* Call to a member function add_rewrite_tag() on a non-object in wp-includes/taxonomy.php
|
19 |
+
*/
|
20 |
+
global $wp_rewrite;
|
21 |
+
|
22 |
+
/**
|
23 |
+
* Overrides the wp_mail() function so we can ensure the messages are
|
24 |
+
* composed when and how they should be
|
25 |
+
*
|
26 |
+
* @uses TestCase::mail_to_file() to store the data for later comparison
|
27 |
+
*/
|
28 |
+
function wp_mail($to, $subject, $message) {
|
29 |
+
TestCase::mail_to_file($to, $subject, $message);
|
30 |
+
}
|
31 |
+
|
32 |
+
/**
|
33 |
+
* Overrides the wp_redirect() function so we can ensure headers are
|
34 |
+
* composed when and how they should be
|
35 |
+
*
|
36 |
+
* @uses TestCase::wp_redirect() to store the data for later comparison
|
37 |
+
*/
|
38 |
+
function wp_redirect($location, $status = 302) {
|
39 |
+
TestCase::wp_redirect($location, $status);
|
40 |
+
}
|
41 |
+
|
42 |
+
/**
|
43 |
+
* Gather the WordPress infrastructure.
|
44 |
+
* Use dirname(dirname()) because safe mode can disable "../".
|
45 |
+
*/
|
46 |
+
$wp_load = dirname(dirname(dirname(dirname(dirname(__FILE__))))) . '/wp-load.php';
|
47 |
+
if (!is_readable($wp_load)) {
|
48 |
+
die("The plugin must be in the 'wp-content/plugins' directory of a working WordPress installation.\n");
|
49 |
+
}
|
50 |
+
require_once $wp_load;
|
51 |
+
|
52 |
+
/**
|
53 |
+
* Get the class we will use for testing
|
54 |
+
*/
|
55 |
+
require_once dirname(__FILE__) . '/Accessor.php';
|
56 |
+
|
57 |
+
/**
|
58 |
+
* Obtain the PHPUnit infrastructure
|
59 |
+
*/
|
60 |
+
require_once 'PHPUnit/Autoload.php';
|
61 |
+
|
62 |
+
/**
|
63 |
+
* Parent TestCase class containing common methods and properties
|
64 |
+
*
|
65 |
+
* @package login-security-solution
|
66 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
67 |
+
* @copyright The Analysis and Solutions Company, 2012
|
68 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
69 |
+
*/
|
70 |
+
abstract class TestCase extends PHPUnit_Framework_TestCase {
|
71 |
+
const USER_PASS = 'aA1!gt%v简E8#';
|
72 |
+
const ID = 'login-security-solution';
|
73 |
+
|
74 |
+
|
75 |
+
/**
|
76 |
+
* Keep PHPUnit from messing up WordPress' crazy use of globals.
|
77 |
+
*
|
78 |
+
* This prevents the following errors:
|
79 |
+
* + Call to a member function add_rule() on a non-object in
|
80 |
+
* wp-includes/rewrite.php
|
81 |
+
* + Call to a member function add_rewrite_tag() on a non-object in
|
82 |
+
* wp-includes/taxonomy.php
|
83 |
+
*/
|
84 |
+
protected $backupGlobals = false;
|
85 |
+
|
86 |
+
/**
|
87 |
+
* Does the current test class touch the database?
|
88 |
+
* @var bool
|
89 |
+
*/
|
90 |
+
protected static $db_needed = true;
|
91 |
+
|
92 |
+
/**
|
93 |
+
* Does the database support transactions?
|
94 |
+
* @var bool
|
95 |
+
*/
|
96 |
+
protected static $db_has_transactions = false;
|
97 |
+
|
98 |
+
/**
|
99 |
+
* Error messges our error handler should expect
|
100 |
+
* @var array
|
101 |
+
* @see TestCase::expected_errors()
|
102 |
+
*/
|
103 |
+
protected $expected_error_list = array();
|
104 |
+
|
105 |
+
/**
|
106 |
+
* Did the expected errors happen?
|
107 |
+
* @var bool
|
108 |
+
* @see TestCase::were_expected_errors_found()
|
109 |
+
*/
|
110 |
+
protected $expected_errors_found = false;
|
111 |
+
|
112 |
+
/**
|
113 |
+
* The actual "Location" header sent by wp_redirect()
|
114 |
+
* @var string
|
115 |
+
*/
|
116 |
+
protected static $location_actual;
|
117 |
+
|
118 |
+
/**
|
119 |
+
* The "Location" header we expect wp_redirect() to send
|
120 |
+
* @var string
|
121 |
+
*/
|
122 |
+
protected static $location_expected;
|
123 |
+
|
124 |
+
/**
|
125 |
+
* @var Accessor
|
126 |
+
*/
|
127 |
+
protected static $lss;
|
128 |
+
|
129 |
+
/**
|
130 |
+
* Name of the mail file
|
131 |
+
* @var string
|
132 |
+
*/
|
133 |
+
protected static $mail_file_basename;
|
134 |
+
|
135 |
+
/**
|
136 |
+
* Path and name of the mail file
|
137 |
+
* @var string
|
138 |
+
*/
|
139 |
+
protected static $mail_file;
|
140 |
+
|
141 |
+
/**
|
142 |
+
* Path to the temporary directory
|
143 |
+
* @var string
|
144 |
+
*/
|
145 |
+
protected static $temp_dir;
|
146 |
+
|
147 |
+
/**
|
148 |
+
* A mockup of the WP_User object
|
149 |
+
* @var WP_User
|
150 |
+
*/
|
151 |
+
protected $user;
|
152 |
+
|
153 |
+
|
154 |
+
/**
|
155 |
+
* Prepares the environment before the first test is run
|
156 |
+
*
|
157 |
+
* NOTE: Not using standard setUpBeforeClass() because we need to have
|
158 |
+
* the child class setting one of this class' static properties
|
159 |
+
*
|
160 |
+
* @return void
|
161 |
+
*/
|
162 |
+
public static function set_up_before_class() {
|
163 |
+
global $wpdb, $wp_object_cache;
|
164 |
+
|
165 |
+
$wp_object_cache = new WP_Object_Cache;
|
166 |
+
|
167 |
+
self::$lss = new Accessor;
|
168 |
+
|
169 |
+
if (self::$db_needed && self::are_transactions_available()) {
|
170 |
+
self::$db_has_transactions = true;
|
171 |
+
self::$lss->activate();
|
172 |
+
$wpdb->query('START TRANSACTION');
|
173 |
+
} else {
|
174 |
+
self::$db_has_transactions = false;
|
175 |
+
}
|
176 |
+
}
|
177 |
+
|
178 |
+
/**
|
179 |
+
* Destroys the environment once the final test is done
|
180 |
+
*/
|
181 |
+
public static function tearDownAfterClass() {
|
182 |
+
global $wpdb;
|
183 |
+
|
184 |
+
if (self::$db_has_transactions) {
|
185 |
+
$wpdb->query('ROLLBACK');
|
186 |
+
self::$lss->deactivate();
|
187 |
+
}
|
188 |
+
|
189 |
+
self::$lss = null;
|
190 |
+
}
|
191 |
+
|
192 |
+
public function setUp() {
|
193 |
+
if (self::$db_needed && !self::$db_has_transactions) {
|
194 |
+
$this->markTestSkipped('Database transactions are needed to test these features, but your "options" and "usermeta" tables are not using the InnoDB engine.');
|
195 |
+
}
|
196 |
+
|
197 |
+
self::$location_actual = null;
|
198 |
+
self::$location_expected = null;
|
199 |
+
self::$mail_file = null;
|
200 |
+
self::$mail_file_basename = null;
|
201 |
+
|
202 |
+
$_SERVER['SERVER_PROTOCOL'] = 'http';
|
203 |
+
|
204 |
+
$this->user = new WP_User;
|
205 |
+
$this->user->ID = 9999999999;
|
206 |
+
$this->user->user_login = 'aaaa';
|
207 |
+
$this->user->user_email = 'bbbb';
|
208 |
+
$this->user->user_url = 'cccc';
|
209 |
+
$this->user->first_name = 'dddd';
|
210 |
+
$this->user->last_name = 'eeee';
|
211 |
+
$this->user->nickname = 'fff@1F*8ffff';
|
212 |
+
$this->user->display_name = '简化字';
|
213 |
+
$this->user->aim = 'hhhhhhhh';
|
214 |
+
$this->user->yim = 'iiiiiiii';
|
215 |
+
$this->user->jabber = 'jjjjjjjj';
|
216 |
+
$this->user->user_pass = self::USER_PASS;
|
217 |
+
}
|
218 |
+
|
219 |
+
public function tearDown() {
|
220 |
+
if (self::$mail_file) {
|
221 |
+
@unlink(self::$mail_file);
|
222 |
+
}
|
223 |
+
$this->expected_error_list = array();
|
224 |
+
restore_error_handler();
|
225 |
+
}
|
226 |
+
|
227 |
+
|
228 |
+
/**
|
229 |
+
* Determines if both the options and usermeta tables use InnoDB
|
230 |
+
* @return bool
|
231 |
+
*/
|
232 |
+
protected static function are_transactions_available() {
|
233 |
+
global $wpdb;
|
234 |
+
|
235 |
+
$opt = $wpdb->get_row("SHOW CREATE TABLE `$wpdb->options`", ARRAY_N);
|
236 |
+
$usr = $wpdb->get_row("SHOW CREATE TABLE `$wpdb->usermeta`", ARRAY_N);
|
237 |
+
|
238 |
+
return (strpos($opt[1], 'ENGINE=InnoDB')
|
239 |
+
&& strpos($usr[1], 'ENGINE=InnoDB'));
|
240 |
+
}
|
241 |
+
|
242 |
+
/**
|
243 |
+
* Examines the last record inserted into the fail table
|
244 |
+
*/
|
245 |
+
protected function check_fail_record($ip, $user_name, $pass_md5) {
|
246 |
+
global $wpdb;
|
247 |
+
|
248 |
+
$this->assertInternalType('integer', $wpdb->insert_id,
|
249 |
+
'This should be an insert id.');
|
250 |
+
|
251 |
+
$sql = 'SELECT *, SYSDATE() AS sysdate
|
252 |
+
FROM `' . self::$lss->table_fail . '`
|
253 |
+
WHERE fail_id = %d';
|
254 |
+
$actual = $wpdb->get_row($wpdb->prepare($sql, $wpdb->insert_id));
|
255 |
+
if (!$actual) {
|
256 |
+
$this->fail('Could not find the record in the "fail" table.');
|
257 |
+
}
|
258 |
+
|
259 |
+
$this->assertEquals($ip, $actual->ip, "'ip' field mismatch.");
|
260 |
+
$this->assertEquals($user_name, $actual->user_login,
|
261 |
+
"'user_name' field mismatch.");
|
262 |
+
$this->assertEquals($pass_md5, $actual->pass_md5,
|
263 |
+
"'pass_md5' field mismatch.");
|
264 |
+
|
265 |
+
$date_failed = new DateTime($actual->date_failed);
|
266 |
+
$sysdate = new DateTime($actual->sysdate);
|
267 |
+
$interval = $date_failed->diff($sysdate);
|
268 |
+
$this->assertLessThanOrEqual('00000000000001',
|
269 |
+
$interval->format('%Y%M%D%H%I%S'),
|
270 |
+
"'date_failed' field off by over 1 second: $actual->date_failed.");
|
271 |
+
}
|
272 |
+
|
273 |
+
/**
|
274 |
+
* @see TestCase::were_expected_errors_found()
|
275 |
+
*/
|
276 |
+
protected function expected_errors($error_messages) {
|
277 |
+
$this->expected_error_list = (array) $error_messages;
|
278 |
+
set_error_handler(array(&$this, 'expected_errors_handler'));
|
279 |
+
}
|
280 |
+
|
281 |
+
/**
|
282 |
+
* @see TestCase::expected_errors()
|
283 |
+
*/
|
284 |
+
protected function were_expected_errors_found() {
|
285 |
+
restore_error_handler();
|
286 |
+
return $this->expected_errors_found;
|
287 |
+
}
|
288 |
+
|
289 |
+
/**
|
290 |
+
* Checks if expected errors were found
|
291 |
+
*/
|
292 |
+
public function expected_errors_handler($errno, $errstr) {
|
293 |
+
foreach ($this->expected_error_list as $expect) {
|
294 |
+
if (strpos($errstr, $expect) !== false) {
|
295 |
+
$this->expected_errors_found = true;
|
296 |
+
return true;
|
297 |
+
}
|
298 |
+
}
|
299 |
+
return false;
|
300 |
+
}
|
301 |
+
|
302 |
+
/**
|
303 |
+
* Writes the "mail" contents to a file for later comparison
|
304 |
+
*/
|
305 |
+
public static function mail_to_file($to, $subject, $message) {
|
306 |
+
if (!self::$temp_dir) {
|
307 |
+
self::$temp_dir = sys_get_temp_dir();
|
308 |
+
}
|
309 |
+
if (!self::$mail_file_basename) {
|
310 |
+
throw new Exception('wp_mail() called at unexpected time'
|
311 |
+
. ' (mail_file_basename was not set).');
|
312 |
+
}
|
313 |
+
self::$mail_file = self::$temp_dir . '/' . self::$mail_file_basename;
|
314 |
+
|
315 |
+
$contents = 'To: ' . implode(', ', (array) $to) . "\n"
|
316 |
+
. "Subject: $subject\n\n$message";
|
317 |
+
|
318 |
+
return file_put_contents(self::$mail_file, $contents);
|
319 |
+
}
|
320 |
+
|
321 |
+
/**
|
322 |
+
* Examines the actual mail file against the expected mail file
|
323 |
+
*/
|
324 |
+
protected function check_mail_file() {
|
325 |
+
if (!self::$mail_file) {
|
326 |
+
$this->fail('wp_mail() has not been called.');
|
327 |
+
}
|
328 |
+
|
329 |
+
$this->assertStringMatchesFormatFile(
|
330 |
+
dirname(__FILE__) . '/expected/' . self::$mail_file_basename,
|
331 |
+
file_get_contents(self::$mail_file)
|
332 |
+
);
|
333 |
+
}
|
334 |
+
|
335 |
+
/**
|
336 |
+
* Writes the location header to a variable for later comparison
|
337 |
+
*/
|
338 |
+
public static function wp_redirect($location, $status) {
|
339 |
+
if (!self::$location_expected) {
|
340 |
+
throw new Exception('wp_redirect() called at unexpected time'
|
341 |
+
. ' ($location_expected was not set).');
|
342 |
+
}
|
343 |
+
self::$location_actual = $location;
|
344 |
+
}
|
345 |
+
}
|
tests/expected/LoginFailTest::test_process_login_fail__post_threshold
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
To: %a
|
2 |
+
Subject: ATTACK HAPPENING TO %a
|
3 |
+
|
4 |
+
Your website, %s, is undergoing a brute force attack.
|
5 |
+
|
6 |
+
There have been at least 4 failed attempts to log in during the past 60 minutes that used one or more of the following components:
|
7 |
+
|
8 |
+
Component Count Value from Current Attempt
|
9 |
+
------------ ----- --------------------------------
|
10 |
+
Network IP 4 1.2.3
|
11 |
+
Username 4 test
|
12 |
+
Password MD5 2 %s
|
13 |
+
|
14 |
+
The Login Security Solution plugin for WordPress is repelling the attack by making their login failures take a very long time.
|
tests/expected/LoginFailTest::test_wp_login__post_breach_threshold
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 4 1.2.3
|
11 |
+
Username 4 test
|
12 |
+
Password MD5 %d %s
|
13 |
+
|
14 |
+
The user has been logged out and will be required to confirm their identity via the password reset functionality.
|
tests/password-validation-profiler.php
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Test speeds of various password tests
|
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 |
+
* Gather the WordPress infrastructure.
|
14 |
+
* Use dirname(dirname()) because safe mode can disable "../".
|
15 |
+
*/
|
16 |
+
require_once dirname(dirname(dirname(dirname(dirname(__FILE__))))) . '/wp-load.php';
|
17 |
+
|
18 |
+
/**
|
19 |
+
* Obtain the plugin class
|
20 |
+
*/
|
21 |
+
require_once dirname(dirname(__FILE__)) . '/login-security-solution.php';
|
22 |
+
$lss = $GLOBALS['login_security_solution'];
|
23 |
+
|
24 |
+
$user = new stdClass;
|
25 |
+
$user->user_login = 'aaaa';
|
26 |
+
$user->user_email = 'bbbb';
|
27 |
+
$user->user_url = 'cccc';
|
28 |
+
$user->first_name = 'dddd';
|
29 |
+
$user->last_name = 'eeee';
|
30 |
+
$user->nickname = 'ffffffff';
|
31 |
+
$user->display_name = 'gggggggg';
|
32 |
+
$user->aim = 'hhhhhhhh';
|
33 |
+
$user->yim = 'iiiiiiii';
|
34 |
+
$user->jabber = 'jjjjjjjj';
|
35 |
+
$user->user_pass = 'aA1!gt%vE8#';
|
36 |
+
|
37 |
+
for ($i = 0; $i < 100; $i++) {
|
38 |
+
$lss->validate_pw($user);
|
39 |
+
}
|
utilities/reduce-dictionary-files.php
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<?php
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Shrinks the size of password dictionary files by removing entries
|
5 |
+
* that fail our other tests
|
6 |
+
*
|
7 |
+
* @package login-security-solution
|
8 |
+
* @author Daniel Convissor <danielc@analysisandsolutions.com>
|
9 |
+
* @copyright The Analysis and Solutions Company, 2012
|
10 |
+
* @license http://www.gnu.org/licenses/gpl-2.0.html GPLv2
|
11 |
+
*/
|
12 |
+
|
13 |
+
/**
|
14 |
+
* Gather the WordPress infrastructure.
|
15 |
+
* Use dirname(dirname()) because safe mode can disable "../".
|
16 |
+
*/
|
17 |
+
require_once dirname(dirname(dirname(dirname(dirname(__FILE__))))) . '/wp-load.php';
|
18 |
+
|
19 |
+
/**
|
20 |
+
* Obtain the plugin class
|
21 |
+
*/
|
22 |
+
require_once dirname(dirname(__FILE__)) . '/login-security-solution.php';
|
23 |
+
$lss = $GLOBALS['login_security_solution'];
|
24 |
+
|
25 |
+
$files = array();
|
26 |
+
$dir = new DirectoryIterator($lss->dir_dictionaries);
|
27 |
+
foreach ($dir as $file) {
|
28 |
+
if ($file->isDir() || $file->getFilename() == 'test.txt') {
|
29 |
+
continue;
|
30 |
+
}
|
31 |
+
$files[$lss->dir_dictionaries . $file->getFilename()] = $file->getSize();
|
32 |
+
}
|
33 |
+
|
34 |
+
// Sort by size.
|
35 |
+
asort($files);
|
36 |
+
|
37 |
+
// Simplify array. For future needs.
|
38 |
+
$file_names = array_keys($files);
|
39 |
+
|
40 |
+
foreach ($file_names as $file) {
|
41 |
+
$fh_old = fopen($file, 'r');
|
42 |
+
$fh_new = fopen("$file.new", 'w');
|
43 |
+
while ($line = fgets($fh_old)) {
|
44 |
+
if ($lss->validate_pw($line)) {
|
45 |
+
$fh_new->fwrite("$line\n");
|
46 |
+
}
|
47 |
+
}
|
48 |
+
fclose($fh_new);
|
49 |
+
fclose($fh_old);
|
50 |
+
rename("$file.new", $file);
|
51 |
+
}
|
52 |
+
|
53 |
+
// The existing tests knock out all of the dictionary passwords.
|
54 |
+
// So don't bother completing the code to reduce things further.
|
55 |
+
exit;
|
56 |
+
|
57 |
+
foreach ($file_names as $file) {
|
58 |
+
$smaller_file = array_shift($file_names);
|
59 |
+
|
60 |
+
if ($file != $smaller_file) {
|
61 |
+
foreach ($file_names as $bigger_file) {
|
62 |
+
// grep
|
63 |
+
}
|
64 |
+
}
|
65 |
+
}
|