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-password-protection.php

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

444 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 * 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' ) as $key ) {
66 $submitted_pass = HTTP::post( $key );
67 if ( ! empty( $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 ( empty( $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 ( empty( $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 ( empty( $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 ( empty( $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 return ! empty( array_intersect( $selected_user_roles, $user_roles ) );
162 }
163
164 /**
165 * Get reset password redirect URL.
166 *
167 * @param WP_User $user The user for whom to generate the URL.
168 *
169 * @return string
170 */
171 public function get_reset_password_redirect_url( $user ): string {
172 $url = '';
173 $key = get_password_reset_key( $user );
174 if ( ! is_wp_error( $key ) ) {
175 if ( wd_di()->get( Woocommerce::class )->is_wc_login_context() && function_exists( 'wc_lostpassword_url' ) ) {
176 $url = add_query_arg(
177 array(
178 'key' => $key,
179 'login' => $user->user_login,
180 ),
181 wc_lostpassword_url()
182 );
183 } else {
184 $url = add_query_arg(
185 array(
186 'action' => 'rp',
187 'key' => $key,
188 'login' => $user->user_login,
189 ),
190 wp_login_url()
191 );
192 }
193 $this->hosting_compatibility( $key, $user->user_login );
194 }
195
196 return $url;
197 }
198
199 /**
200 * Sets a cookie notice.
201 *
202 * @param string $name The name of the cookie.
203 * @param string $value The value of the cookie.
204 * @param int $time The expiration time of the cookie.
205 */
206 public function set_cookie_notice( $name, $value, $time ) {
207 if ( ! isset( $_COOKIE[ $name ] ) ) {
208 setcookie( $name, $value, $time, '/' );
209 }
210 }
211
212 /**
213 * Remove cookie notice.
214 * Setting the expiration time in the past indicates to the browser that the cookie has expired and should be
215 * removed.
216 *
217 * @param string $name The name of the cookie.
218 *
219 * @return void
220 * @since 4.3.0 Removed the $time parameter.
221 */
222 public function remove_cookie_notice( string $name ): void {
223 if ( isset( $_COOKIE[ $name ] ) && ! headers_sent() ) {
224 setcookie( $name, '', time() - YEAR_IN_SECONDS, '/' );
225 }
226 $this->remove_extra_cookies();
227 }
228
229 /**
230 * Gets the last time the user's password was changed.
231 *
232 * @param WP_User|int $user The user to check.
233 *
234 * @return int The timestamp of the last password change.
235 */
236 protected function password_last_changed( $user ) {
237 if ( ! $user ) {
238 return 0;
239 }
240
241 $changed = (int) get_user_meta( $user->ID, 'wd_last_password_change', true );
242
243 if ( ! $changed ) {
244 return (int) strtotime( $user->user_registered );
245 }
246
247 return $changed;
248 }
249
250 /**
251 * Checks if the user's password is expired.
252 *
253 * @param WP_User $user The user to check.
254 *
255 * @return bool True if the password is expired, false otherwise.
256 */
257 public function check_expired_password( $user ) {
258 return isset( $this->model->force_time ) && $this->model->force_time >= $this->password_last_changed( $user );
259 }
260
261 /**
262 * Updates the last password change time when a user's password is updated.
263 *
264 * @param WP_User $user The user whose password was updated.
265 */
266 public function handle_password_updated( $user ) {
267 update_user_meta( $user->ID, 'wd_last_password_change', time() );
268 }
269
270 /**
271 * Handles compatibility with different hosting environments when resetting passwords.
272 *
273 * @param string $key The reset key.
274 * @param string $user_login The user's login name.
275 */
276 protected function hosting_compatibility( $key, $user_login ) {
277 $mask_login = new \WP_Defender\Model\Setting\Mask_Login();
278 if ( ! isset( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ) && $mask_login->is_active() && 'flywheel' === Server::get_current_server() ) {
279 $value = sprintf( '%s:%s', $user_login, $key );
280 setcookie(
281 'wp-resetpass-' . COOKIEHASH,
282 $value,
283 0,
284 $mask_login->get_new_login_url(),
285 COOKIE_DOMAIN,
286 is_ssl(),
287 true
288 );
289 }
290 }
291
292 /**
293 * Removes extra cookies set during the password reset process.
294 */
295 protected function remove_extra_cookies() {
296 if ( isset( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ) && 'flywheel' === Server::get_current_server() && ! headers_sent() ) {
297 setcookie(
298 'wp-resetpass-' . COOKIEHASH,
299 '',
300 time() - YEAR_IN_SECONDS,
301 COOKIEPATH,
302 COOKIE_DOMAIN,
303 is_ssl(),
304 true
305 );
306 }
307 }
308
309 /**
310 * Force reset password.
311 *
312 * @param WP_User|WP_Error $user WP_User object or WP_Error.
313 * @param string $password Password plain string.
314 *
315 * @return WP_User|WP_Error Return user object or error object.
316 */
317 public function do_force_reset( $user, $password ) {
318 // The check for $user and $password was performed before the method was called.
319 if (
320 wp_check_password( $password, $user->user_pass, $user->ID )
321 && $this->is_force_reset( $user )
322 ) {
323 $action = 'password_reset';
324 $cookie_name = 'display_reset_password_warning';
325
326 $this->trigger_redirect( $user, $action, $cookie_name );
327 }
328
329 return $user;
330 }
331
332 /**
333 * Reset weak password.
334 *
335 * @param WP_User|WP_Error $user WP_User object or WP_Error.
336 * @param string $password Password plain string.
337 *
338 * @return WP_User|WP_Error Return user object or error object.
339 */
340 public function do_weak_reset( $user, $password ) {
341 // The check for $user and $password was performed before the method was called.
342 if (
343 wp_check_password( $password, $user->user_pass, $user->ID )
344 && $this->is_weak_password( $user, $password )
345 ) {
346 $action = 'password_protection';
347 $cookie_name = 'display_pwned_password_warning';
348
349 $this->trigger_redirect( $user, $action, $cookie_name );
350 }
351
352 return $user;
353 }
354
355 /**
356 * Redirect to reset password with error message.
357 *
358 * @param WP_User|WP_Error $user WP_User object or WP_Error.
359 * @param string $action Action query string name.
360 * @param string $cookie_name Cookie name.
361 */
362 public function trigger_redirect( $user, $action, $cookie_name ) {
363 // Set cookie to check and display the warning notice on reset password page.
364 $this->set_cookie_notice( $cookie_name, true, time() + MINUTE_IN_SECONDS * 2 );
365 // Get the reset password URL.
366 $url = $this->get_reset_password_redirect_url( $user );
367 /**
368 * Fires before redirecting to the password reset page.
369 *
370 * @param string $url
371 * @param string $action
372 *
373 * @since 2.5.6
374 */
375 do_action( 'wd_forced_reset_password_url', $url, $action );
376 // Redirect to the reset password page.
377 if ( '' !== $url ) {
378 $url = esc_url_raw( $url );
379 header( 'Cache-Control: no-store, no-cache' );
380 wp_safe_redirect( $url );
381 exit();
382 }
383 }
384
385 /**
386 * Add WooCommerce error message for password reset warnings.
387 *
388 * @param string $wc_message WooCommerce default error message.
389 * @return string WooCommerce error message.
390 */
391 public function add_woocommerce_error_message( $wc_message ) {
392 if ( isset( $_COOKIE['display_pwned_password_warning'] ) && function_exists( 'wc_print_notice' ) ) {
393 wc_print_notice( $this->password_protection_model->pwned_actions['force_change_message'], 'error' );
394 $this->remove_cookie_notice( 'display_pwned_password_warning' );
395 }
396 if ( isset( $_COOKIE['display_reset_password_warning'] ) && function_exists( 'wc_print_notice' ) ) {
397 wc_print_notice( $this->model->message, 'error' );
398 $this->remove_cookie_notice( 'display_reset_password_warning' );
399 }
400 return $wc_message;
401 }
402
403 /**
404 * Verify if password is weak.
405 * Is user role enabled for weak password verification and is the password weak?
406 * If so then return true else password is strong therefore return false.
407 *
408 * @param WP_User $user WP_User object.
409 * @param string $password Plain password string.
410 *
411 * @return bool If password weak then true else false.
412 * @since 2.6.1
413 */
414 public function is_weak_password( $user, $password ) {
415 $user_roles = $this->password_protection_model->user_roles;
416 $is_enabled_by_user = $this->is_enabled_by_user_role( $user, $user_roles );
417 $is_pwned = $this->check_pwned_password( $password );
418
419 $is_weak_password = $this->password_protection_model->is_active() && $is_enabled_by_user && ! is_wp_error( $is_pwned ) && $is_pwned;
420
421 return $is_weak_password;
422 }
423
424 /**
425 * Verify if password is need to be force reset.
426 * Is user role enabled for force password reset and is the password expired?
427 * If so then return true else password is non expired therefore return false.
428 *
429 * @param WP_User $user WP_User object.
430 *
431 * @return bool If password expired then true else false.
432 * @since 2.6.1
433 */
434 public function is_force_reset( $user ) {
435 $user_roles = $this->model->user_roles;
436 $is_enabled_by_user = $this->is_enabled_by_user_role( $user, $user_roles );
437 $is_expired = $this->check_expired_password( $user );
438
439 $is_expired_password = $this->model->is_active() && $is_enabled_by_user && $is_expired;
440
441 return $is_expired_password;
442 }
443 }
444