PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.1
Defender Security – Malware Scanner, Login Security & Firewall v5.8.1
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / class-strong-password.php

class-strong-password.php in Defender Security – Malware Scanner, Login Security & Firewall 5.8.1, at src/component/class-strong-password.php

453 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Enforces strong password policies for user accounts.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use WP_User;
11 use WP_Error;
12 use stdClass;
13 use WP_Defender\Component;
14 use WP_Defender\Traits\User;
15 use WP_Defender\Integrations\Woocommerce;
16 use WP_Defender\Model\Setting\Strong_Password as Settings;
17
18 /**
19 * Enforces strong password policies for user accounts.
20 */
21 class Strong_Password extends Component {
22 use User;
23
24 /**
25 * Cookie key to show warning message on reset password page.
26 *
27 * @var string
28 */
29 protected const COOKIE_KEY = 'display_strong_password_warning';
30
31 /**
32 * Error code.
33 *
34 * @var string
35 */
36 protected const CODE = 'password_strength';
37
38 /**
39 * Password reset settings model.
40 *
41 * @var Settings|null
42 */
43 protected ?Settings $model;
44
45 /**
46 * Helper instance for reuse methods from pwned password protection.
47 *
48 * @var Password_Protection|null
49 */
50 protected ?Password_Protection $helper;
51
52 /**
53 * WooCommerce integration instance.
54 *
55 * @var Woocommerce|null
56 */
57 protected ?Woocommerce $woo;
58
59 /**
60 * Flag to track if cookie has been handled.
61 *
62 * @var bool
63 */
64 private $cookie_handled = false;
65
66 /**
67 * Constructs the object, setting the model to a Settings instance.
68 */
69 public function __construct() {
70 $this->model = wd_di()->get( Settings::class );
71 $this->helper = wd_di()->get( Password_Protection::class );
72 $this->woo = wd_di()->get( Woocommerce::class );
73 }
74
75 /**
76 * Filters whether the given user can be authenticated with the provided password.
77 *
78 * @param WP_User|WP_Error $user WP_User or WP_Error object if a previous callback failed authentication.
79 * @param string $password Password to check against the user.
80 *
81 * @return WP_User|WP_Error WP_User or WP_Error object if a previous callback failed authentication.
82 */
83 public function during_core_authentication( $user, $password ) {
84 // Check if the user object is valid.
85 if ( is_wp_error( $user ) ) {
86 return $user;
87 }
88
89 // Check if the password is empty.
90 if ( '' === $password ) {
91 return new WP_Error(
92 'defender_invalid_password',
93 esc_html__( 'Invalid user data.', 'defender-security' )
94 );
95 }
96
97 // Verify the provided password against the user's password.
98 if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
99 return $user;
100 }
101
102 // Check if the user's role requires strong password enforcement.
103 if ( ! $this->should_enforce_for_user( $user, $this->model ) ) {
104 return $user;
105 }
106
107 // Validate the strength of the provided password.
108 if ( $this->is_weak_password( $password ) ) {
109 $this->helper->trigger_redirect( $user, self::CODE, self::COOKIE_KEY );
110 exit;
111 }
112
113 return $user;
114 }
115
116 /**
117 * Check if a password is weak.
118 *
119 * Password requirements:
120 * - At least 12 characters long.
121 * - Must contain both uppercase and lowercase letters.
122 * - At least one symbol.
123 * - At least one number.
124 *
125 * @param string $password The password to validate.
126 *
127 * @return bool True if the password is weak, false otherwise.
128 */
129 private function is_weak_password( $password ): bool {
130 // Check minimum length.
131 if ( strlen( $password ) < 12 ) {
132 return true;
133 }
134
135 // Check for at least one uppercase letter.
136 if ( ! preg_match( '/[A-Z]/', $password ) ) {
137 return true;
138 }
139
140 // Check for at least one lowercase letter.
141 if ( ! preg_match( '/[a-z]/', $password ) ) {
142 return true;
143 }
144
145 // Check for at least one number.
146 if ( ! preg_match( '/[0-9]/', $password ) ) {
147 return true;
148 }
149
150 // Check for at least one special character or symbol.
151 $symbol_pattern = '/[!@#$%^&*()_+\-={};:\'",.<>?~\[\]\/|`]/';
152 if ( ! preg_match( $symbol_pattern, $password ) ) {
153 return true;
154 }
155
156 return false;
157 }
158
159 /**
160 * Enqueue custom password strength script on relevant admin pages.
161 *
162 * @param string $hook_suffix The current admin page.
163 */
164 public function scripts( $hook_suffix ) {
165 // Check if the current admin page is relevant.
166 if ( '' === $hook_suffix ) {
167 global $pagenow;
168 $hook_suffix = $pagenow;
169 }
170
171 if ( 'user-edit.php' === $hook_suffix ) {
172 // Changes for another existing user? Skip case for a new user on /wp-admin/user-new.php.
173 global $profile_user;
174 $user_obj = $profile_user;
175 } elseif ( 'profile.php' === $hook_suffix ) {
176 // Changes for the current user.
177 global $current_user;
178 $user_obj = $current_user;
179 } else {
180 $user_obj = null;
181 }
182 // Prevent strong password enforcement for exempt users.
183 if ( isset( $user_obj ) && ! $this->should_enforce_for_user( $user_obj, $this->model ) ) {
184 return;
185 }
186 global $user;
187 if ( isset( $user ) && ! $this->should_enforce_for_user( $user, $this->model ) ) {
188 return;
189 }
190 // Collect all locations.
191 $pages = array( 'profile.php', 'user-new.php', 'user-edit.php', 'wp-login.php' );
192 if ( $this->woo->is_activated() ) {
193 $pages[] = 'index.php';
194 }
195
196 if ( in_array( $hook_suffix, $pages, true ) ) {
197 wp_dequeue_script( 'wc-password-strength-meter' );
198 wp_enqueue_style( 'wd-strong-password', plugins_url( 'assets/css/strong-password.css', WP_DEFENDER_FILE ), array( 'dashicons' ), DEFENDER_VERSION );
199 wp_enqueue_script(
200 'wd-strong-password',
201 plugins_url( 'assets/js/strong-password.js', WP_DEFENDER_FILE ),
202 array(
203 'jquery',
204 'password-strength-meter',
205 ),
206 DEFENDER_VERSION,
207 true
208 );
209 wp_localize_script(
210 'wd-strong-password',
211 'wpdef_pws_strings',
212 array(
213 'message' => esc_html__( 'Hint: Your password must follow the guidelines below.', 'defender-security' ),
214 'requirements' => array(
215 'length' => esc_html__( 'At least 12 characters', 'defender-security' ),
216 'case' => esc_html__( 'Uppercase and lowercase letters', 'defender-security' ),
217 'symbol' => esc_html__( 'At least one symbol', 'defender-security' ),
218 'number' => esc_html__( 'At least one number', 'defender-security' ),
219 'zxcvbn' => esc_html__( 'Avoid common words or sequences of letters/numbers', 'defender-security' ),
220 ),
221 )
222 );
223 add_filter( 'password_hint', '__return_empty_string' );
224 }
225 }
226
227 /**
228 * Validate password during user profile update.
229 *
230 * @param WP_Error $errors WP_Error object.
231 * @param bool $update Whether this is a user update.
232 * @param stdClass|WP_User $user User object.
233 */
234 public function on_profile_update( $errors, $update, $user ) {
235 if (
236 // If there are already errors with the password, exit.
237 '' !== $errors->get_error_message( 'pass' ) ||
238 // If the user's password is not set, exit.
239 ! isset( $user->user_pass )
240 ) {
241 return;
242 }
243
244 // When updating the profile check if user's role preference is enabled.
245 if ( ! $this->should_enforce_for_user( $user, $this->model ) ) {
246 return;
247 }
248
249 $login_password = $this->helper->get_submitted_password();
250
251 // Check if the submitted password is weak.
252 if ( '' !== $login_password && $this->is_weak_password( $login_password ) ) {
253 $errors->add( self::CODE, $this->model->get_message() );
254 }
255 }
256
257 /**
258 * Validate password during password reset.
259 *
260 * @param WP_Error $errors WP_Error object.
261 * @param WP_User|null $user WP_User object.
262 */
263 public function on_password_reset( $errors, $user = null ) {
264 if ( null === $user ) {
265 return;
266 }
267
268 if ( ! $this->should_enforce_for_user( $user, $this->model ) ) {
269 return;
270 }
271
272 // Check if display_strong_password_warning cookie enabled then show warning message on reset password page.
273 $cookie_value = isset( $_COOKIE[ self::COOKIE_KEY ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_KEY ] ) ) : '';
274
275 // If the cookie is set, show the warning message.
276 if ( '' !== $cookie_value ) {
277 $errors->add( self::CODE, $this->model->get_message() );
278
279 // Remove the cookie.
280 $this->helper->remove_cookie_notice( self::COOKIE_KEY );
281 $this->cookie_handled = true;
282 }
283
284 $submitted_password = $this->helper->get_submitted_password();
285
286 // Check if the submitted password is weak.
287 if (
288 '' !== $submitted_password &&
289 $this->is_weak_password( $submitted_password ) &&
290 ! in_array( self::CODE, $errors->get_error_codes(), true )
291 ) {
292 $errors->add( self::CODE, $this->model->get_message() );
293 }
294 }
295
296 /**
297 * Generate a strong password with a mix of character types.
298 *
299 * @param string $password The password to be generated.
300 * @param int $length The length of the password to be generated (12 or 24).
301 *
302 * @return string Strong password.
303 */
304 public function generate_password( $password, $length ) {
305 if ( ! in_array( $length, array( 12, 24 ), true ) ) {
306 return $password;
307 }
308
309 $upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
310 $lower = 'abcdefghijklmnopqrstuvwxyz';
311 $numbers = '0123456789';
312 $symbols = '!@#$%^&*()_+-={};:\'",.<>?~[]|`';
313
314 // Ensure the password includes at least one character from each set to meet strength requirements.
315 $password = '';
316 $password .= $upper[ random_int( 0, strlen( $upper ) - 1 ) ];
317 $password .= $lower[ random_int( 0, strlen( $lower ) - 1 ) ];
318 $password .= $numbers[ random_int( 0, strlen( $numbers ) - 1 ) ];
319 $password .= $symbols[ random_int( 0, strlen( $symbols ) - 1 ) ];
320
321 // Fill the rest of the password length with random characters from all character sets.
322 $all = $upper . $lower . $numbers . $symbols;
323 for ( $i = strlen( $password ); $i < $length; $i++ ) {
324 $password .= $all[ random_int( 0, strlen( $all ) - 1 ) ];
325 }
326
327 // Shuffle the password to prevent predictable character order.
328 return str_shuffle( $password );
329 }
330
331 /**
332 * Add WooCommerce error message for password reset warnings.
333 *
334 * @param string $wc_message WooCommerce default error message.
335 * @return string WooCommerce error message.
336 */
337 public function add_woocommerce_error_message( $wc_message ) {
338 if ( isset( $_COOKIE[ self::COOKIE_KEY ] ) && function_exists( 'wc_print_notice' ) && ! $this->cookie_handled ) {
339 $message = $this->model->get_message();
340 wc_print_notice( $message, 'error' );
341 $this->helper->remove_cookie_notice( self::COOKIE_KEY );
342 }
343 return $wc_message;
344 }
345
346 /**
347 * Validate password strength during WooCommerce checkout registration.
348 *
349 * @param WP_Error $errors Error object.
350 *
351 * @return WP_Error Modified error object.
352 */
353 public function during_woo_registration( $errors ) {
354 $password = $this->helper->get_submitted_password();
355
356 if (
357 '' !== $password &&
358 $this->is_weak_password( $password ) &&
359 ! in_array( self::CODE, $errors->get_error_codes(), true )
360 ) {
361 $errors->add( self::CODE, $this->model->get_message() );
362 }
363
364 return $errors;
365 }
366
367 /**
368 * Validate password strength during WooCommerce account details save.
369 *
370 * @param WP_Error $errors WP_Error object.
371 * @param WP_User|stdClass $user User object.
372 */
373 public function on_woo_account_update( $errors, $user ) {
374 if ( ! $user instanceof stdClass || ! isset( $user->user_pass ) ) {
375 return;
376 }
377
378 if ( ! $this->should_enforce_for_user( $user, $this->model ) ) {
379 return;
380 }
381
382 $password = $this->helper->get_submitted_password();
383
384 if (
385 '' !== $password &&
386 $this->is_weak_password( $password ) &&
387 ! in_array( self::CODE, $errors->get_error_codes(), true )
388 ) {
389 $errors->add( self::CODE, $this->model->get_message() );
390 }
391 }
392
393 /**
394 * Validate password strength during WooCommerce login.
395 *
396 * @param WP_Error $validation_error WP_Error object to collect validation errors.
397 * @param string $username User login name.
398 * @param string $password User password.
399 *
400 * @return WP_User|WP_Error WP_User or WP_Error object with any validation errors added.
401 */
402 public function during_woo_authentication( $validation_error, $username, $password ) {
403 if ( '' === $password ) {
404 return $validation_error;
405 }
406
407 $user = get_user_by( 'login', $username );
408 if ( ! $user ) {
409 $user = get_user_by( 'email', $username );
410 }
411
412 if ( ! $user ) {
413 return $validation_error;
414 }
415
416 if ( ! $this->should_enforce_for_user( $user, $this->model ) ) {
417 return $validation_error;
418 }
419
420 return $this->during_core_authentication( $user, $password );
421 }
422
423 /**
424 * Modify the lost password URL.
425 *
426 * @param string $redirect The redirect URL.
427 *
428 * @return string Modified lost password URL.
429 */
430 public function lostpassword_url( $redirect = '' ) {
431 $args = array(
432 'action' => 'lostpassword',
433 );
434
435 if ( '' !== $redirect ) {
436 $args['redirect_to'] = rawurlencode( $redirect );
437 }
438
439 if ( is_multisite() ) {
440 $blog_details = get_site();
441 if ( null !== $blog_details ) {
442 $wp_login_path = $blog_details->path . 'wp-login.php';
443 } else {
444 $wp_login_path = 'wp-login.php';
445 }
446 } else {
447 $wp_login_path = 'wp-login.php';
448 }
449
450 return add_query_arg( $args, network_site_url( $wp_login_path, 'login' ) );
451 }
452 }
453