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

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

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