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

307 lines 8.8 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 if ( in_array( $hook_suffix, $pages, true ) ) {
177 wp_enqueue_style( 'wd-strong-password', plugins_url( 'assets/css/strong-password.css', WP_DEFENDER_FILE ), array( 'dashicons' ), DEFENDER_VERSION );
178 wp_enqueue_script(
179 'wd-strong-password',
180 plugins_url( 'assets/js/strong-password.js', WP_DEFENDER_FILE ),
181 array(
182 'jquery',
183 'password-strength-meter',
184 ),
185 DEFENDER_VERSION,
186 true
187 );
188 wp_localize_script(
189 'wd-strong-password',
190 'wpdef_pws_strings',
191 array(
192 'message' => esc_html__( 'Hint: Your password must follow the guidelines below.', 'defender-security' ),
193 'requirements' => array(
194 'length' => esc_html__( 'At least 12 characters', 'defender-security' ),
195 'case' => esc_html__( 'Uppercase and lowercase letters', 'defender-security' ),
196 'symbol' => esc_html__( 'At least one symbol', 'defender-security' ),
197 'number' => esc_html__( 'At least one number', 'defender-security' ),
198 'zxcvbn' => esc_html__( 'Avoid common words or sequences of letters/numbers', 'defender-security' ),
199 ),
200 )
201 );
202 add_filter( 'password_hint', '__return_empty_string' );
203 }
204 }
205
206 /**
207 * Validate password during user profile update.
208 *
209 * @param WP_Error $errors WP_Error object.
210 * @param bool $update Whether this is a user update.
211 * @param stdClass|WP_User $user User object.
212 */
213 public function on_profile_update( $errors, $update, $user ) {
214 if (
215 // If there are already errors with the password, exit.
216 $errors->get_error_message( 'pass' ) ||
217 // If the user object is invalid, exit.
218 is_wp_error( $user ) ||
219 // If the user's password is not set, exit.
220 ! isset( $user->user_pass )
221 ) {
222 return;
223 }
224
225 // When updating the profile check if user's role preference is enabled.
226 if ( ! $this->should_enforce_for_user( $user ) ) {
227 return;
228 }
229
230 $login_password = $this->helper->get_submitted_password();
231
232 // Check if the submitted password is weak.
233 if ( isset( $login_password ) && $this->is_weak_password( $login_password ) ) {
234 $errors->add( self::CODE, $this->model->get_message() );
235 }
236 }
237
238 /**
239 * Validate password during password reset.
240 *
241 * @param WP_Error $errors WP_Error object.
242 * @param WP_User $user WP_User object.
243 */
244 public function on_password_reset( $errors, $user ) {
245 if ( is_wp_error( $user ) ) {
246 return;
247 }
248
249 if ( ! $this->should_enforce_for_user( $user ) ) {
250 return;
251 }
252
253 // Check if display_strong_password_warning cookie enabled then show warning message on reset password page.
254 $cookie_value = isset( $_COOKIE[ self::COOKIE_KEY ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_KEY ] ) ) : '';
255
256 // If the cookie is set, show the warning message.
257 if ( ! empty( $cookie_value ) ) {
258 $errors->add( self::CODE, $this->model->get_message() );
259
260 // Remove the cookie.
261 $this->helper->remove_cookie_notice( self::COOKIE_KEY );
262 }
263
264 $submitted_password = $this->helper->get_submitted_password();
265
266 // Check if the submitted password is weak.
267 if ( ! empty( $submitted_password ) && $this->is_weak_password( $submitted_password ) ) {
268 $errors->add( self::CODE, $this->model->get_message() );
269 }
270 }
271
272 /**
273 * Generate a strong password with a mix of character types.
274 *
275 * @param string $password The password to be generated.
276 * @param int $length The length of the password to be generated (12 or 24).
277 *
278 * @return string Strong password.
279 */
280 public function generate_password( $password, $length ) {
281 if ( ! in_array( $length, array( 12, 24 ), true ) ) {
282 return $password;
283 }
284
285 $upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
286 $lower = 'abcdefghijklmnopqrstuvwxyz';
287 $numbers = '0123456789';
288 $symbols = '!@#$%^&*()_+-={};:\'",.<>?~[]|`';
289
290 // Ensure the password includes at least one character from each set to meet strength requirements.
291 $password = '';
292 $password .= $upper[ random_int( 0, strlen( $upper ) - 1 ) ];
293 $password .= $lower[ random_int( 0, strlen( $lower ) - 1 ) ];
294 $password .= $numbers[ random_int( 0, strlen( $numbers ) - 1 ) ];
295 $password .= $symbols[ random_int( 0, strlen( $symbols ) - 1 ) ];
296
297 // Fill the rest of the password length with random characters from all character sets.
298 $all = $upper . $lower . $numbers . $symbols;
299 for ( $i = strlen( $password ); $i < $length; $i++ ) {
300 $password .= $all[ random_int( 0, strlen( $all ) - 1 ) ];
301 }
302
303 // Shuffle the password to prevent predictable character order.
304 return str_shuffle( $password );
305 }
306 }
307