PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.1
Defender Security – Malware Scanner, Login Security & Firewall v6.2.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-password-protection.php

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

466 lines 13.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles the logic for the Pwned Passwords module including password checks against the Pwned Passwords API.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use WP_User;
11 use WP_Error;
12 use Calotes\Helper\HTTP;
13 use WP_Defender\Component;
14 use WP_Defender\Integrations\Woocommerce;
15 use WP_Defender\Model\Setting\Password_Reset;
16 use WP_Defender\Component\Security_Tweaks\Servers\Server;
17 use WP_Defender\Model\Setting\Password_Protection as Password_Protection_Settings;
18
19 /**
20 * Handles the logic for the Pwned Passwords module including password checks against the Pwned Passwords API.
21 */
22 class Password_Protection extends Component {
23
24 public const PASSWORD_LOG = 'password.log';
25
26 /**
27 * The Pwned API URL.
28 * API source website: http://haveibeenpwned.com/. API version: v3.
29 *
30 * @var string
31 */
32 protected $pwned_api;
33
34 /**
35 * Password reset settings model.
36 *
37 * @var Password_Reset
38 */
39 protected $model;
40
41 /**
42 * Use for cache.
43 *
44 * @var Password_Protection_Settings
45 */
46 protected $password_protection_model;
47
48 /**
49 * Constructor for initializing the Password_Protection component.
50 */
51 public function __construct() {
52 $this->pwned_api = 'https://api.pwnedpasswords.com/range/';
53 $this->model = wd_di()->get( Password_Reset::class );
54
55 $this->password_protection_model = wd_di()->get( Password_Protection_Settings::class );
56 }
57
58 /**
59 * Get the password that was submitted by user.
60 *
61 * @return string
62 */
63 public function get_submitted_password() {
64 $password = '';
65 foreach ( array( 'pwd', 'pass1', 'password', 'edd_user_pass', 'password_1', 'account_password' ) as $key ) {
66 $submitted_pass = HTTP::post( $key );
67 if ( '' !== $submitted_pass ) {
68 $password = $submitted_pass;
69 break;
70 }
71 }
72
73 return $password;
74 }
75
76 /**
77 * Makes an API request to the remote server and checks if the password is pwned.
78 *
79 * @param string $password The password to check.
80 *
81 * @return WP_Error|bool
82 */
83 public function check_pwned_password( $password ) {
84 $hash = strtoupper( hash( 'sha1', $password ) );
85 $subhash = substr( $hash, 0, 5 );
86
87 $url = $this->pwned_api . $subhash;
88 $args = array(
89 'method' => 'GET',
90 'headers' => array(
91 'user-agent' => defender_get_own_user_agent(),
92 ),
93 );
94
95 $request = wp_remote_request( $url, $args );
96
97 if ( is_wp_error( $request ) ) {
98 return $request;
99 }
100
101 $pwned_count = 0;
102 $body = wp_remote_retrieve_body( $request );
103 foreach ( array_map( 'trim', explode( "\n", trim( $body ) ) ) as $row ) {
104 if ( $subhash . substr( strtoupper( $row ), 0, 35 ) === $hash ) {
105 $pwned_count = substr( $row, 36 );
106 break;
107 }
108 }
109
110 // Found Pwned password or not.
111 return $pwned_count > 0;
112 }
113
114 /**
115 * Check if the specified user role is enabled.
116 *
117 * @param WP_User $user The user to check.
118 * @param array $selected_user_roles The roles to check against.
119 *
120 * @return bool
121 */
122 public function is_enabled_by_user_role( $user, $selected_user_roles ) {
123 // No for super admin.
124 if ( is_multisite() && is_super_admin( $user->ID ) ) {
125 return false;
126 }
127
128 if ( ! isset( $user->roles ) || array() === $user->roles ) {
129 // WP_User does have data about roles sometimes, e.g. on the Profile page, so we get it from userdata.
130 $user_id = $user->ID;
131 if ( ! is_multisite() ) {
132 $user_meta = get_userdata( $user_id );
133 if ( ! isset( $user_meta->roles ) || array() === $user_meta->roles ) {
134 // User should have roles.
135 $this->log( sprintf( "User ID: %d doesn't have roles.", $user_id ), self::PASSWORD_LOG );
136
137 return false;
138 }
139 $user_roles = $user_meta->roles;
140 } else {
141 $arr_user_blogs = get_blogs_of_user( $user_id );
142 if ( ! is_array( $arr_user_blogs ) || array() === $arr_user_blogs ) {
143 // User should be associated with some site.
144 $this->log( sprintf( 'User ID: %d is not associated with any site.', $user_id ), self::PASSWORD_LOG );
145
146 return false;
147 }
148 $user_blog_id = array_key_first( $arr_user_blogs );
149 $user = new WP_User( $user_id, '', $user_blog_id );
150 if ( ! isset( $user->roles ) || array() === $user->roles ) {
151 $this->log( sprintf( "User ID: %d doesn't have roles on MU.", $user->ID ), self::PASSWORD_LOG );
152
153 return false;
154 }
155 $user_roles = $user->roles;
156 }
157 } else {
158 $user_roles = $user->roles;
159 }
160
161 $common_roles = array_intersect( $selected_user_roles, $user_roles );
162 return array() !== $common_roles;
163 }
164
165 /**
166 * Get reset password redirect URL.
167 *
168 * @param WP_User $user The user for whom to generate the URL.
169 *
170 * @return string
171 */
172 public function get_reset_password_redirect_url( $user ): string {
173 $url = '';
174 $key = get_password_reset_key( $user );
175 if ( ! is_wp_error( $key ) ) {
176 if ( wd_di()->get( Woocommerce::class )->is_wc_login_context() && function_exists( 'wc_lostpassword_url' ) ) {
177 $url = add_query_arg(
178 array(
179 'key' => $key,
180 'login' => $user->user_login,
181 ),
182 wc_lostpassword_url()
183 );
184 } else {
185 $url = add_query_arg(
186 array(
187 'action' => 'rp',
188 'key' => $key,
189 'login' => $user->user_login,
190 ),
191 wp_login_url()
192 );
193 }
194 $this->hosting_compatibility( $key, $user->user_login );
195 }
196
197 return $url;
198 }
199
200 /**
201 * Sets a cookie notice.
202 *
203 * @param string $name The name of the cookie.
204 * @param string $value The value of the cookie.
205 * @param int $time The expiration time of the cookie.
206 */
207 public function set_cookie_notice( $name, $value, $time ) {
208 if ( ! isset( $_COOKIE[ $name ] ) ) {
209 setcookie(
210 $name,
211 $value,
212 array(
213 'expires' => $time,
214 'path' => '/',
215 'secure' => is_ssl(),
216 'httponly' => true,
217 'samesite' => 'Lax',
218 )
219 );
220 }
221 }
222
223 /**
224 * Remove cookie notice.
225 * Setting the expiration time in the past indicates to the browser that the cookie has expired and should be
226 * removed.
227 *
228 * @param string $name The name of the cookie.
229 *
230 * @return void
231 * @since 4.3.0 Removed the $time parameter.
232 */
233 public function remove_cookie_notice( string $name ): void {
234 if ( isset( $_COOKIE[ $name ] ) && ! headers_sent() ) {
235 setcookie(
236 $name,
237 '',
238 array(
239 'expires' => time() - YEAR_IN_SECONDS,
240 'path' => '/',
241 'secure' => is_ssl(),
242 'httponly' => true,
243 'samesite' => 'Lax',
244 )
245 );
246 }
247 $this->remove_extra_cookies();
248 }
249
250 /**
251 * Gets the last time the user's password was changed.
252 *
253 * @param WP_User|int $user The user to check.
254 *
255 * @return int The timestamp of the last password change.
256 */
257 protected function password_last_changed( $user ) {
258 if ( ! $user ) {
259 return 0;
260 }
261
262 $changed = (int) get_user_meta( $user->ID, 'wd_last_password_change', true );
263
264 if ( ! $changed ) {
265 $changed = strtotime( $user->user_registered );
266 $changed = ! is_int( $changed ) ? (int) $changed : $changed;
267 }
268
269 return $changed;
270 }
271
272 /**
273 * Checks if the user's password is expired.
274 *
275 * @param WP_User $user The user to check.
276 *
277 * @return bool True if the password is expired, false otherwise.
278 */
279 public function check_expired_password( $user ) {
280 return isset( $this->model->force_time ) && $this->model->force_time >= $this->password_last_changed( $user );
281 }
282
283 /**
284 * Updates the last password change time when a user's password is updated.
285 *
286 * @param WP_User $user The user whose password was updated.
287 */
288 public function handle_password_updated( $user ) {
289 update_user_meta( $user->ID, 'wd_last_password_change', time() );
290 }
291
292 /**
293 * Handles compatibility with different hosting environments when resetting passwords.
294 *
295 * @param string $key The reset key.
296 * @param string $user_login The user's login name.
297 */
298 protected function hosting_compatibility( $key, $user_login ) {
299 $mask_login = new \WP_Defender\Model\Setting\Mask_Login();
300 if ( ! isset( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ) && $mask_login->is_active() && 'flywheel' === Server::get_current_server() ) {
301 $value = sprintf( '%s:%s', $user_login, $key );
302 setcookie(
303 'wp-resetpass-' . COOKIEHASH,
304 $value,
305 0,
306 $mask_login->get_new_login_url(),
307 COOKIE_DOMAIN,
308 is_ssl(),
309 true
310 );
311 }
312 }
313
314 /**
315 * Removes extra cookies set during the password reset process.
316 */
317 protected function remove_extra_cookies() {
318 if ( isset( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ) && 'flywheel' === Server::get_current_server() && ! headers_sent() ) {
319 setcookie(
320 'wp-resetpass-' . COOKIEHASH,
321 '',
322 time() - YEAR_IN_SECONDS,
323 COOKIEPATH,
324 COOKIE_DOMAIN,
325 is_ssl(),
326 true
327 );
328 }
329 }
330
331 /**
332 * Force reset password.
333 *
334 * @param WP_User|WP_Error $user WP_User object or WP_Error.
335 * @param string $password Password plain string.
336 *
337 * @return WP_User|WP_Error Return user object or error object.
338 */
339 public function do_force_reset( $user, $password ) {
340 // The check for $user and $password was performed before the method was called.
341 if (
342 wp_check_password( $password, $user->user_pass, $user->ID )
343 && $this->is_force_reset( $user )
344 ) {
345 $action = 'password_reset';
346 $cookie_name = 'display_reset_password_warning';
347
348 $this->trigger_redirect( $user, $action, $cookie_name );
349 }
350
351 return $user;
352 }
353
354 /**
355 * Reset weak password.
356 *
357 * @param WP_User|WP_Error $user WP_User object or WP_Error.
358 * @param string $password Password plain string.
359 *
360 * @return WP_User|WP_Error Return user object or error object.
361 */
362 public function do_weak_reset( $user, $password ) {
363 // The check for $user and $password was performed before the method was called.
364 if (
365 wp_check_password( $password, $user->user_pass, $user->ID )
366 && $this->is_weak_password( $user, $password )
367 ) {
368 $action = 'password_protection';
369 $cookie_name = 'display_pwned_password_warning';
370
371 $this->trigger_redirect( $user, $action, $cookie_name );
372 }
373
374 return $user;
375 }
376
377 /**
378 * Redirect to reset password with error message.
379 *
380 * @param WP_User|WP_Error $user WP_User object or WP_Error.
381 * @param string $action Action query string name.
382 * @param string $cookie_name Cookie name.
383 */
384 public function trigger_redirect( $user, $action, $cookie_name ) {
385 // Set cookie to check and display the warning notice on reset password page.
386 $this->set_cookie_notice( $cookie_name, true, time() + MINUTE_IN_SECONDS * 2 );
387 // Get the reset password URL.
388 $url = $this->get_reset_password_redirect_url( $user );
389 /**
390 * Fires before redirecting to the password reset page.
391 *
392 * @param string $url
393 * @param string $action
394 *
395 * @since 2.5.6
396 */
397 do_action( 'wd_forced_reset_password_url', $url, $action );
398 // Redirect to the reset password page.
399 if ( '' !== $url ) {
400 $url = esc_url_raw( $url );
401 header( 'Cache-Control: no-store, no-cache' );
402 wp_safe_redirect( $url );
403 exit();
404 }
405 }
406
407 /**
408 * Add WooCommerce error message for password reset warnings.
409 *
410 * @param string $wc_message WooCommerce default error message.
411 * @return string WooCommerce error message.
412 */
413 public function add_woocommerce_error_message( $wc_message ) {
414 if ( isset( $_COOKIE['display_pwned_password_warning'] ) && function_exists( 'wc_print_notice' ) ) {
415 wc_print_notice( $this->password_protection_model->pwned_actions['force_change_message'], 'error' );
416 $this->remove_cookie_notice( 'display_pwned_password_warning' );
417 }
418 if ( isset( $_COOKIE['display_reset_password_warning'] ) && function_exists( 'wc_print_notice' ) ) {
419 wc_print_notice( $this->model->message, 'error' );
420 $this->remove_cookie_notice( 'display_reset_password_warning' );
421 }
422 return $wc_message;
423 }
424
425 /**
426 * Verify if password is weak.
427 * Is user role enabled for weak password verification and is the password weak?
428 * If so then return true else password is strong therefore return false.
429 *
430 * @param WP_User $user WP_User object.
431 * @param string $password Plain password string.
432 *
433 * @return bool If password weak then true else false.
434 * @since 2.6.1
435 */
436 public function is_weak_password( $user, $password ) {
437 $user_roles = $this->password_protection_model->user_roles;
438 $is_enabled_by_user = $this->is_enabled_by_user_role( $user, $user_roles );
439 $is_pwned = $this->check_pwned_password( $password );
440
441 $is_weak_password = $this->password_protection_model->is_active() && $is_enabled_by_user && ! is_wp_error( $is_pwned ) && $is_pwned;
442
443 return $is_weak_password;
444 }
445
446 /**
447 * Verify if password is need to be force reset.
448 * Is user role enabled for force password reset and is the password expired?
449 * If so then return true else password is non expired therefore return false.
450 *
451 * @param WP_User $user WP_User object.
452 *
453 * @return bool If password expired then true else false.
454 * @since 2.6.1
455 */
456 public function is_force_reset( $user ) {
457 $user_roles = $this->model->user_roles;
458 $is_enabled_by_user = $this->is_enabled_by_user_role( $user, $user_roles );
459 $is_expired = $this->check_expired_password( $user );
460
461 $is_expired_password = $this->model->is_active() && $is_enabled_by_user && $is_expired;
462
463 return $is_expired_password;
464 }
465 }
466