PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.5.1
Defender Security – Malware Scanner, Login Security & Firewall v5.5.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.5.1, at src/component/class-strong-password.php

325 lines 9.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\Model\Setting\Strong_Password as Settings;
16
17 /**
18 * Enforces strong password policies for user accounts.
19 */
20 class Strong_Password extends Component {
21 use User;
22
23 /**
24 * Cookie key to show warning message on reset password page.
25 *
26 * @var string
27 */
28 protected const COOKIE_KEY = 'display_strong_password_warning';
29
30 /**
31 * Error code.
32 *
33 * @var string
34 */
35 protected const CODE = 'password_strength';
36
37 /**
38 * Password reset settings model.
39 *
40 * @var Settings|null
41 */
42 protected ?Settings $model;
43
44 /**
45 * Helper instance for reuse methods from pwned password protection.
46 *
47 * @var Password_Protection|null
48 */
49 protected ?Password_Protection $helper;
50
51 /**
52 * Constructs the object, setting the model to a Settings instance.
53 */
54 public function __construct() {
55 $this->model = wd_di()->get( Settings::class );
56 $this->helper = wd_di()->get( Password_Protection::class );
57 }
58
59 /**
60 * Filters whether the given user can be authenticated with the provided password.
61 *
62 * @param WP_User|WP_Error $user WP_User or WP_Error object if a previous callback failed authentication.
63 * @param string $password Password to check against the user.
64 *
65 * @return WP_User|WP_Error WP_User or WP_Error object if a previous callback failed authentication.
66 */
67 public function during_authentication( $user, $password ) {
68 // Check if the user object is valid.
69 if ( is_wp_error( $user ) || ! $user instanceof WP_User ) {
70 return $user;
71 }
72
73 // Check if the password is empty.
74 if ( empty( $password ) ) {
75 return new WP_Error(
76 'defender_invalid_password',
77 esc_html__( 'Invalid user data.', 'defender-security' )
78 );
79 }
80
81 // Verify the provided password against the user's password.
82 if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
83 return $user;
84 }
85
86 // Check if the user's role requires strong password enforcement.
87 if ( ! $this->should_enforce_for_user( $user ) ) {
88 return $user;
89 }
90
91 // Validate the strength of the provided password.
92 if ( $this->is_weak_password( $password ) ) {
93 $this->helper->trigger_redirect( $user, self::CODE, self::COOKIE_KEY );
94 exit;
95 }
96
97 return $user;
98 }
99
100 /**
101 * Check if a password is weak.
102 *
103 * Password requirements:
104 * - At least 12 characters long.
105 * - Must contain both uppercase and lowercase letters.
106 * - At least one symbol.
107 * - At least one number.
108 *
109 * @param string $password The password to validate.
110 *
111 * @return bool True if the password is weak, false otherwise.
112 */
113 private function is_weak_password( $password ): bool {
114 // Check minimum length.
115 if ( strlen( $password ) < 12 ) {
116 return true;
117 }
118
119 // Check for at least one uppercase letter.
120 if ( ! preg_match( '/[A-Z]/', $password ) ) {
121 return true;
122 }
123
124 // Check for at least one lowercase letter.
125 if ( ! preg_match( '/[a-z]/', $password ) ) {
126 return true;
127 }
128
129 // Check for at least one number.
130 if ( ! preg_match( '/[0-9]/', $password ) ) {
131 return true;
132 }
133
134 // Check for at least one special character or symbol.
135 $symbol_pattern = '/[!@#$%^&*()_+\-={};:\'",.<>?~\[\]\/|`]/';
136 if ( ! preg_match( $symbol_pattern, $password ) ) {
137 return true;
138 }
139
140 return false;
141 }
142
143 /**
144 * Enqueue custom password strength script on relevant admin pages.
145 *
146 * @param string $hook_suffix The current admin page.
147 */
148 public function scripts( $hook_suffix ) {
149 // Check if the current admin page is relevant.
150 if ( '' === $hook_suffix ) {
151 global $pagenow;
152 $hook_suffix = $pagenow;
153 }
154
155 if ( 'user-edit.php' === $hook_suffix ) {
156 // Changes for another existing user? Skip case for a new user on /wp-admin/user-new.php.
157 global $profile_user;
158 $user_obj = $profile_user;
159 } elseif ( 'profile.php' === $hook_suffix ) {
160 // Changes for the current user.
161 global $current_user;
162 $user_obj = $current_user;
163 } else {
164 $user_obj = null;
165 }
166 // Prevent strong password enforcement for exempt users.
167 if ( isset( $user_obj ) && ! $this->should_enforce_for_user( $user_obj ) ) {
168 return;
169 }
170 global $user;
171 if ( isset( $user ) && ! $this->should_enforce_for_user( $user ) ) {
172 return;
173 }
174 // Collect all locations.
175 $pages = array( 'profile.php', 'user-new.php', 'user-edit.php', 'wp-login.php' );
176
177 $is_wc_context = wd_di()->get( \WP_Defender\Integrations\Woocommerce::class )->is_wc_login_context();
178 if ( in_array( $hook_suffix, $pages, true ) || $is_wc_context ) {
179 wp_dequeue_script( 'wc-password-strength-meter' );
180 wp_enqueue_style( 'wd-strong-password', plugins_url( 'assets/css/strong-password.css', WP_DEFENDER_FILE ), array( 'dashicons' ), DEFENDER_VERSION );
181 wp_enqueue_script(
182 'wd-strong-password',
183 plugins_url( 'assets/js/strong-password.js', WP_DEFENDER_FILE ),
184 array(
185 'jquery',
186 'password-strength-meter',
187 ),
188 DEFENDER_VERSION,
189 true
190 );
191 wp_localize_script(
192 'wd-strong-password',
193 'wpdef_pws_strings',
194 array(
195 'message' => esc_html__( 'Hint: Your password must follow the guidelines below.', 'defender-security' ),
196 'requirements' => array(
197 'length' => esc_html__( 'At least 12 characters', 'defender-security' ),
198 'case' => esc_html__( 'Uppercase and lowercase letters', 'defender-security' ),
199 'symbol' => esc_html__( 'At least one symbol', 'defender-security' ),
200 'number' => esc_html__( 'At least one number', 'defender-security' ),
201 'zxcvbn' => esc_html__( 'Avoid common words or sequences of letters/numbers', 'defender-security' ),
202 ),
203 )
204 );
205 add_filter( 'password_hint', '__return_empty_string' );
206 }
207 }
208
209 /**
210 * Validate password during user profile update.
211 *
212 * @param WP_Error $errors WP_Error object.
213 * @param bool $update Whether this is a user update.
214 * @param stdClass|WP_User $user User object.
215 */
216 public function on_profile_update( $errors, $update, $user ) {
217 if (
218 // If there are already errors with the password, exit.
219 $errors->get_error_message( 'pass' ) ||
220 // If the user object is invalid, exit.
221 is_wp_error( $user ) ||
222 // If the user's password is not set, exit.
223 ! isset( $user->user_pass )
224 ) {
225 return;
226 }
227
228 // When updating the profile check if user's role preference is enabled.
229 if ( ! $this->should_enforce_for_user( $user ) ) {
230 return;
231 }
232
233 $login_password = $this->helper->get_submitted_password();
234
235 // Check if the submitted password is weak.
236 if ( isset( $login_password ) && $this->is_weak_password( $login_password ) ) {
237 $errors->add( self::CODE, $this->model->get_message() );
238 }
239 }
240
241 /**
242 * Validate password during password reset.
243 *
244 * @param WP_Error $errors WP_Error object.
245 * @param WP_User $user WP_User object.
246 */
247 public function on_password_reset( $errors, $user ) {
248 if ( is_wp_error( $user ) ) {
249 return;
250 }
251
252 if ( ! $this->should_enforce_for_user( $user ) ) {
253 return;
254 }
255
256 // Check if display_strong_password_warning cookie enabled then show warning message on reset password page.
257 $cookie_value = isset( $_COOKIE[ self::COOKIE_KEY ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_KEY ] ) ) : '';
258
259 // If the cookie is set, show the warning message.
260 if ( ! empty( $cookie_value ) ) {
261 $errors->add( self::CODE, $this->model->get_message() );
262
263 // Remove the cookie.
264 $this->helper->remove_cookie_notice( self::COOKIE_KEY );
265 }
266
267 $submitted_password = $this->helper->get_submitted_password();
268
269 // Check if the submitted password is weak.
270 if ( ! empty( $submitted_password ) && $this->is_weak_password( $submitted_password ) ) {
271 $errors->add( self::CODE, $this->model->get_message() );
272 }
273 }
274
275 /**
276 * Generate a strong password with a mix of character types.
277 *
278 * @param string $password The password to be generated.
279 * @param int $length The length of the password to be generated (12 or 24).
280 *
281 * @return string Strong password.
282 */
283 public function generate_password( $password, $length ) {
284 if ( ! in_array( $length, array( 12, 24 ), true ) ) {
285 return $password;
286 }
287
288 $upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
289 $lower = 'abcdefghijklmnopqrstuvwxyz';
290 $numbers = '0123456789';
291 $symbols = '!@#$%^&*()_+-={};:\'",.<>?~[]|`';
292
293 // Ensure the password includes at least one character from each set to meet strength requirements.
294 $password = '';
295 $password .= $upper[ random_int( 0, strlen( $upper ) - 1 ) ];
296 $password .= $lower[ random_int( 0, strlen( $lower ) - 1 ) ];
297 $password .= $numbers[ random_int( 0, strlen( $numbers ) - 1 ) ];
298 $password .= $symbols[ random_int( 0, strlen( $symbols ) - 1 ) ];
299
300 // Fill the rest of the password length with random characters from all character sets.
301 $all = $upper . $lower . $numbers . $symbols;
302 for ( $i = strlen( $password ); $i < $length; $i++ ) {
303 $password .= $all[ random_int( 0, strlen( $all ) - 1 ) ];
304 }
305
306 // Shuffle the password to prevent predictable character order.
307 return str_shuffle( $password );
308 }
309
310 /**
311 * Add WooCommerce error message for password reset warnings.
312 *
313 * @param string $wc_message WooCommerce default error message.
314 * @return string WooCommerce error message.
315 */
316 public function add_woocommerce_error_message( $wc_message ) {
317 if ( isset( $_COOKIE[ self::COOKIE_KEY ] ) && function_exists( 'wc_print_notice' ) ) {
318 $message = $this->model->get_message();
319 wc_print_notice( $message, 'error' );
320 $this->helper->remove_cookie_notice( self::COOKIE_KEY );
321 }
322 return $wc_message;
323 }
324 }
325