PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.4
Defender Security – Malware Scanner, Login Security & Firewall v6.2.4
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 6.2.4, at src/component/class-strong-password.php

482 lines 13.7 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 // Skip enforcement on WooCommerce requests — dedicated Woo filters handle them based on settings.
108 if ( $this->is_woo_request() ) {
109 return $user;
110 }
111
112 // Validate the strength of the provided password.
113 if ( $this->is_weak_password( $password ) ) {
114 $this->helper->trigger_redirect( $user, self::CODE, self::COOKIE_KEY );
115 exit;
116 }
117
118 return $user;
119 }
120
121 /**
122 * Check if a password is weak.
123 *
124 * Password requirements:
125 * - At least 12 characters long.
126 * - Must contain both uppercase and lowercase letters.
127 * - At least one symbol.
128 * - At least one number.
129 *
130 * @param string $password The password to validate.
131 *
132 * @return bool True if the password is weak, false otherwise.
133 */
134 private function is_weak_password( $password ): bool {
135 // Check minimum length.
136 if ( strlen( $password ) < 12 ) {
137 return true;
138 }
139
140 // Check for at least one uppercase letter.
141 if ( ! preg_match( '/[A-Z]/', $password ) ) {
142 return true;
143 }
144
145 // Check for at least one lowercase letter.
146 if ( ! preg_match( '/[a-z]/', $password ) ) {
147 return true;
148 }
149
150 // Check for at least one number.
151 if ( ! preg_match( '/[0-9]/', $password ) ) {
152 return true;
153 }
154
155 // Check for at least one special character or symbol.
156 $symbol_pattern = '/[!@#$%^&*()_+\-={};:\'",.<>?~\[\]\/|`]/';
157 if ( ! preg_match( $symbol_pattern, $password ) ) {
158 return true;
159 }
160
161 return false;
162 }
163
164 /**
165 * Enqueue custom password strength script on relevant admin pages.
166 *
167 * @param string $hook_suffix The current admin page.
168 */
169 public function scripts( $hook_suffix ) {
170 // Check if the current admin page is relevant.
171 if ( '' === $hook_suffix ) {
172 global $pagenow;
173 $hook_suffix = $pagenow;
174 }
175
176 if ( 'user-edit.php' === $hook_suffix ) {
177 // Changes for another existing user? Skip case for a new user on /wp-admin/user-new.php.
178 global $profile_user;
179 $user_obj = $profile_user;
180 } elseif ( 'profile.php' === $hook_suffix ) {
181 // Changes for the current user.
182 global $current_user;
183 $user_obj = $current_user;
184 } else {
185 $user_obj = null;
186 }
187 // Prevent strong password enforcement for exempt users.
188 if ( isset( $user_obj ) && ! $this->should_enforce_for_user( $user_obj, $this->model ) ) {
189 return;
190 }
191 global $user;
192 if ( isset( $user ) && ! $this->should_enforce_for_user( $user, $this->model ) ) {
193 return;
194 }
195 // Collect all locations.
196 $pages = array( 'profile.php', 'user-new.php', 'user-edit.php', 'wp-login.php' );
197 if ( $this->woo->is_activated() ) {
198 $pages[] = 'index.php';
199 }
200
201 if ( in_array( $hook_suffix, $pages, true ) ) {
202 wp_dequeue_script( 'wc-password-strength-meter' );
203 wp_enqueue_style( 'wd-strong-password', plugins_url( 'assets/css/strong-password.css', WP_DEFENDER_FILE ), array( 'dashicons' ), DEFENDER_VERSION );
204 wp_enqueue_script(
205 'wd-strong-password',
206 plugins_url( 'assets/js/strong-password.js', WP_DEFENDER_FILE ),
207 array(
208 'jquery',
209 'password-strength-meter',
210 ),
211 DEFENDER_VERSION,
212 true
213 );
214 wp_localize_script(
215 'wd-strong-password',
216 'wpdef_pws_strings',
217 array(
218 'message' => esc_html__( 'Hint: Your password must follow the guidelines below.', 'defender-security' ),
219 'requirements' => array(
220 'length' => esc_html__( 'At least 12 characters', 'defender-security' ),
221 'case' => esc_html__( 'Uppercase and lowercase letters', 'defender-security' ),
222 'symbol' => esc_html__( 'At least one symbol', 'defender-security' ),
223 'number' => esc_html__( 'At least one number', 'defender-security' ),
224 'zxcvbn' => esc_html__( 'Avoid common words or sequences of letters/numbers', 'defender-security' ),
225 ),
226 'woo_locations' => isset( $this->model->forms['woocommerce'] ) && is_array( $this->model->forms['woocommerce'] )
227 ? $this->model->forms['woocommerce']
228 : array(),
229 )
230 );
231 add_filter( 'password_hint', '__return_empty_string' );
232 }
233 }
234
235 /**
236 * Validate password during user profile update.
237 *
238 * @param WP_Error $errors WP_Error object.
239 * @param bool $update Whether this is a user update.
240 * @param stdClass|WP_User $user User object.
241 */
242 public function on_profile_update( $errors, $update, $user ) {
243 if (
244 // If there are already errors with the password, exit.
245 '' !== $errors->get_error_message( 'pass' ) ||
246 // If the user's password is not set, exit.
247 ! isset( $user->user_pass )
248 ) {
249 return;
250 }
251
252 // When updating the profile check if user's role preference is enabled.
253 if ( ! $this->should_enforce_for_user( $user, $this->model ) ) {
254 return;
255 }
256
257 $login_password = $this->helper->get_submitted_password();
258
259 // Check if the submitted password is weak.
260 if ( '' !== $login_password && $this->is_weak_password( $login_password ) ) {
261 $errors->add( self::CODE, $this->model->get_message() );
262 }
263 }
264
265 /**
266 * Validate password during password reset.
267 *
268 * @param WP_Error $errors WP_Error object.
269 * @param WP_User|null $user WP_User object.
270 */
271 public function on_password_reset( $errors, $user = null ) {
272 if ( null === $user ) {
273 return;
274 }
275
276 if ( ! $this->should_enforce_for_user( $user, $this->model ) ) {
277 return;
278 }
279
280 // WooCommerce also fires this core hook — gate on the lost-password form setting.
281 if ( $this->is_woo_request() && ! $this->is_woo_form_enabled( Woocommerce::WOO_LOST_PASSWORD_FORM ) ) {
282 return;
283 }
284
285 // Check if display_strong_password_warning cookie enabled then show warning message on reset password page.
286 $cookie_value = isset( $_COOKIE[ self::COOKIE_KEY ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_KEY ] ) ) : '';
287
288 // If the cookie is set, show the warning message.
289 if ( '' !== $cookie_value ) {
290 $errors->add( self::CODE, $this->model->get_message() );
291
292 // Remove the cookie.
293 $this->helper->remove_cookie_notice( self::COOKIE_KEY );
294 $this->cookie_handled = true;
295 }
296
297 $submitted_password = $this->helper->get_submitted_password();
298
299 // Check if the submitted password is weak.
300 if (
301 '' !== $submitted_password &&
302 $this->is_weak_password( $submitted_password ) &&
303 ! in_array( self::CODE, $errors->get_error_codes(), true )
304 ) {
305 $errors->add( self::CODE, $this->model->get_message() );
306 }
307 }
308
309 /**
310 * Generate a strong password with a mix of character types.
311 *
312 * @param string $password The password to be generated.
313 * @param int $length The length of the password to be generated (12 or 24).
314 *
315 * @return string Strong password.
316 */
317 public function generate_password( $password, $length ) {
318 if ( ! in_array( $length, array( 12, 24 ), true ) ) {
319 return $password;
320 }
321
322 $upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
323 $lower = 'abcdefghijklmnopqrstuvwxyz';
324 $numbers = '0123456789';
325 $symbols = '!@#$%^&*()_+-={};:\'",.<>?~[]|`';
326
327 // Ensure the password includes at least one character from each set to meet strength requirements.
328 $password = '';
329 $password .= $upper[ random_int( 0, strlen( $upper ) - 1 ) ];
330 $password .= $lower[ random_int( 0, strlen( $lower ) - 1 ) ];
331 $password .= $numbers[ random_int( 0, strlen( $numbers ) - 1 ) ];
332 $password .= $symbols[ random_int( 0, strlen( $symbols ) - 1 ) ];
333
334 // Fill the rest of the password length with random characters from all character sets.
335 $all = $upper . $lower . $numbers . $symbols;
336 for ( $i = strlen( $password ); $i < $length; $i++ ) {
337 $password .= $all[ random_int( 0, strlen( $all ) - 1 ) ];
338 }
339
340 // Shuffle the password to prevent predictable character order.
341 return str_shuffle( $password );
342 }
343
344 /**
345 * Add WooCommerce error message for password reset warnings.
346 *
347 * @param string $wc_message WooCommerce default error message.
348 * @return string WooCommerce error message.
349 */
350 public function add_woocommerce_error_message( $wc_message ) {
351 if ( ! isset( $this->model->forms['woocommerce'] ) ) {
352 return $wc_message;
353 }
354
355 if ( isset( $_COOKIE[ self::COOKIE_KEY ] ) && function_exists( 'wc_print_notice' ) && ! $this->cookie_handled ) {
356 $message = $this->model->get_message();
357 wc_print_notice( $message, 'error' );
358 $this->helper->remove_cookie_notice( self::COOKIE_KEY );
359 }
360 return $wc_message;
361 }
362
363 /**
364 * Validate password strength during WooCommerce checkout registration.
365 *
366 * @param WP_Error $errors Error object.
367 *
368 * @return WP_Error Modified error object.
369 */
370 public function during_woo_registration( $errors ) {
371 if ( ! isset( $this->model->forms['woocommerce'] ) || ! is_array( $this->model->forms['woocommerce'] ) || ! in_array( Woocommerce::WOO_REGISTER_FORM, $this->model->forms['woocommerce'], true ) ) {
372 return $errors;
373 }
374
375 $password = $this->helper->get_submitted_password();
376
377 if (
378 '' !== $password &&
379 $this->is_weak_password( $password ) &&
380 ! in_array( self::CODE, $errors->get_error_codes(), true )
381 ) {
382 $errors->add( self::CODE, $this->model->get_message() );
383 }
384
385 return $errors;
386 }
387
388 /**
389 * Validate password strength during WooCommerce account details save.
390 *
391 * @param WP_Error $errors WP_Error object.
392 * @param WP_User|stdClass $user User object.
393 */
394 public function on_woo_account_update( $errors, $user ) {
395 if ( ! $user instanceof stdClass || ! isset( $user->user_pass ) ) {
396 return;
397 }
398
399 if ( ! $this->should_enforce_for_user( $user, $this->model ) ) {
400 return;
401 }
402
403 if ( ! isset( $this->model->forms['woocommerce'] ) || ! is_array( $this->model->forms['woocommerce'] ) ) {
404 return;
405 }
406
407 $password = $this->helper->get_submitted_password();
408
409 if (
410 '' !== $password &&
411 $this->is_weak_password( $password ) &&
412 ! in_array( self::CODE, $errors->get_error_codes(), true )
413 ) {
414 $errors->add( self::CODE, $this->model->get_message() );
415 }
416 }
417
418 /**
419 * Validate password strength during WooCommerce login.
420 *
421 * @param WP_Error $validation_error WP_Error object to collect validation errors.
422 * @param string $username User login name.
423 * @param string $password User password.
424 *
425 * @return WP_User|WP_Error WP_User or WP_Error object with any validation errors added.
426 */
427 public function during_woo_authentication( $validation_error, $username, $password ) {
428 if ( '' === $password ) {
429 return $validation_error;
430 }
431
432 if ( $this->is_woo_request() && ! $this->is_woo_form_enabled( Woocommerce::WOO_LOGIN_FORM ) ) {
433 return $validation_error;
434 }
435 $user = get_user_by( 'login', $username );
436 if ( ! $user ) {
437 $user = get_user_by( 'email', $username );
438 }
439
440 if ( ! $user ) {
441 return $validation_error;
442 }
443
444 if ( ! $this->should_enforce_for_user( $user, $this->model ) ) {
445 return $validation_error;
446 }
447
448 if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
449 return $validation_error;
450 }
451
452 if ( $this->is_weak_password( $password ) ) {
453 $this->helper->trigger_redirect( $user, self::CODE, self::COOKIE_KEY );
454 exit;
455 }
456
457 return $validation_error;
458 }
459
460 /**
461 * Determine if current request is coming from WooCommerce flows.
462 */
463 private function is_woo_request(): bool {
464 return $this->woo->is_activated() && $this->woo->is_wc_login_context();
465 }
466
467 /**
468 * Check whether a specific WooCommerce form is enabled in settings.
469 *
470 * @param string $form Form slug.
471 */
472 private function is_woo_form_enabled( string $form ): bool {
473 if ( ! isset( $this->model->plugins['woocommerce'] ) || ! $this->model->plugins['woocommerce'] ) {
474 return false;
475 }
476
477 $forms = $this->model->forms['woocommerce'] ?? array();
478
479 return in_array( $form, $forms, true );
480 }
481 }
482