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

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