File: /home/slyfwmm/foar/wp-content/plugins/defender-security/src/component/class-password-protection.php
<?php
/**
* Handles the logic for the Pwned Passwords module including password checks against the Pwned Passwords API.
*
* @package WP_Defender\Component
*/
namespace WP_Defender\Component;
use WP_User;
use WP_Error;
use Calotes\Helper\HTTP;
use WP_Defender\Component;
use WP_Defender\Model\Setting\Password_Reset;
use WP_Defender\Component\Security_Tweaks\Servers\Server;
use WP_Defender\Model\Setting\Password_Protection as Password_Protection_Settings;
/**
* Handles the logic for the Pwned Passwords module including password checks against the Pwned Passwords API.
*/
class Password_Protection extends Component {
/**
* The Pwned API URL.
* API source website: http://haveibeenpwned.com/. API version: v3.
*
* @var string
*/
protected $pwned_api;
/**
* Password reset settings model.
*
* @var Password_Reset
*/
protected $model;
/**
* Use for cache.
*
* @var Password_Protection_Settings
*/
protected $password_protection_model;
/**
* Constructor for initializing the Password_Protection component.
*/
public function __construct() {
$this->pwned_api = 'https://api.pwnedpasswords.com/range/';
$this->model = wd_di()->get( Password_Reset::class );
$this->password_protection_model = wd_di()->get( Password_Protection_Settings::class );
}
/**
* Get the password that was submitted by user.
*
* @return string
*/
public function get_submitted_password() {
$password = '';
foreach ( array( 'pwd', 'pass1', 'password', 'edd_user_pass' ) as $key ) {
$submitted_pass = HTTP::post( $key );
if ( ! empty( $submitted_pass ) ) {
$password = $submitted_pass;
break;
}
}
return $password;
}
/**
* Makes an API request to the remote server and checks if the password is pwned.
*
* @param string $password The password to check.
*
* @return WP_Error|bool
*/
public function check_pwned_password( $password ) {
$hash = strtoupper( hash( 'sha1', $password ) );
$subhash = substr( $hash, 0, 5 );
$url = $this->pwned_api . $subhash;
$args = array(
'method' => 'GET',
'headers' => array(
'user-agent' => sprintf(
'Mozilla/5.0 (compatible; WPMU DEV Defender/%1$s; +https://wpmudev.com)',
DEFENDER_VERSION
),
),
);
$request = wp_remote_request( $url, $args );
if ( is_wp_error( $request ) ) {
return $request;
}
$pwned_count = 0;
$body = wp_remote_retrieve_body( $request );
foreach ( array_map( 'trim', explode( "\n", trim( $body ) ) ) as $row ) {
if ( $subhash . substr( strtoupper( $row ), 0, 35 ) === $hash ) {
$pwned_count = substr( $row, 36 );
break;
}
}
// Found Pwned password or not.
return $pwned_count > 0;
}
/**
* Check if the specified user role is enabled.
*
* @param WP_User $user The user to check.
* @param array $selected_user_roles The roles to check against.
*
* @return bool
*/
public function is_enabled_by_user_role( $user, $selected_user_roles ) {
// No for super admin.
if ( is_multisite() && is_super_admin( $user->ID ) ) {
return false;
}
if ( empty( $user->roles ) ) {
// WP_User does have data about roles sometimes, e.g. on the Profile page, so we get it from userdata.
$user_id = $user->ID;
if ( ! is_multisite() ) {
$user_meta = get_userdata( $user_id );
if ( empty( $user_meta->roles ) ) {
// User should have roles.
$this->log( sprintf( "User ID: %d doesn't have roles.", $user_id ), 'password.log' );
return false;
}
$user_roles = $user_meta->roles;
} else {
$arr_user_blogs = get_blogs_of_user( $user_id );
if ( empty( $arr_user_blogs ) ) {
// User should be associated with some site.
$this->log( sprintf( 'User ID: %d is not associated with any site.', $user_id ), 'password.log' );
return false;
}
$user_blog_id = array_key_first( $arr_user_blogs );
$user = new WP_User( $user_id, '', $user_blog_id );
if ( empty( $user->roles ) ) {
$this->log( sprintf( "User ID: %d doesn't have roles on MU.", $user->ID ), 'password.log' );
return false;
}
$user_roles = $user->roles;
}
} else {
$user_roles = $user->roles;
}
return ! empty( array_intersect( $selected_user_roles, $user_roles ) );
}
/**
* Get reset password redirect URL.
*
* @param WP_User $user The user for whom to generate the URL.
*
* @return string|null
*/
public function get_reset_password_redirect_url( $user ) {
$url = null;
$key = get_password_reset_key( $user );
if ( ! is_wp_error( $key ) ) {
$url = add_query_arg(
array(
'action' => 'rp',
'key' => $key,
'login' => $user->user_login,
),
wp_login_url()
);
// Extra hosting checks.
$this->hosting_compatibility( $key, $user->user_login );
}
return $url;
}
/**
* Reset password redirect.
*
* @param string|null $url The URL to redirect to.
* @param bool $safe Whether to use safe redirect.
*
* @return void
*/
public function reset_password_redirect( $url, $safe = false ) {
if ( empty( $url ) ) {
return;
}
$url = esc_url_raw( $url );
header( 'Cache-Control: no-store, no-cache' );
$safe ? wp_safe_redirect( $url ) : wp_redirect( $url ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
exit();
}
/**
* Sets a cookie notice.
*
* @param string $name The name of the cookie.
* @param string $value The value of the cookie.
* @param int $time The expiration time of the cookie.
*/
public function set_cookie_notice( $name, $value, $time ) {
if ( ! isset( $_COOKIE[ $name ] ) ) {
setcookie( $name, $value, $time, '/' );
}
}
/**
* Remove cookie notice.
* Setting the expiration time in the past indicates to the browser that the cookie has expired and should be
* removed.
*
* @param string $name The name of the cookie.
*
* @return void
* @since 4.3.0 Removed the $time parameter.
*/
public function remove_cookie_notice( string $name ): void {
if ( isset( $_COOKIE[ $name ] ) ) {
setcookie( $name, '', time() - YEAR_IN_SECONDS, '/' );
}
$this->remove_extra_cookies();
}
/**
* Gets the last time the user's password was changed.
*
* @param WP_User|int $user The user to check.
*
* @return int The timestamp of the last password change.
*/
protected function password_last_changed( $user ) {
if ( ! $user ) {
return 0;
}
$changed = (int) get_user_meta( $user->ID, 'wd_last_password_change', true );
if ( ! $changed ) {
return (int) strtotime( $user->user_registered );
}
return $changed;
}
/**
* Checks if the user's password is expired.
*
* @param WP_User $user The user to check.
*
* @return bool True if the password is expired, false otherwise.
*/
public function check_expired_password( $user ) {
return isset( $this->model->force_time ) && $this->model->force_time >= $this->password_last_changed( $user );
}
/**
* Updates the last password change time when a user's password is updated.
*
* @param WP_User $user The user whose password was updated.
*/
public function handle_password_updated( $user ) {
update_user_meta( $user->ID, 'wd_last_password_change', time() );
}
/**
* Handles compatibility with different hosting environments when resetting passwords.
*
* @param string $key The reset key.
* @param string $user_login The user's login name.
*/
protected function hosting_compatibility( $key, $user_login ) {
$mask_login = new \WP_Defender\Model\Setting\Mask_Login();
if ( ! isset( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ) && $mask_login->is_active() && 'flywheel' === Server::get_current_server() ) {
$value = sprintf( '%s:%s', $user_login, $key );
setcookie(
'wp-resetpass-' . COOKIEHASH,
$value,
0,
$mask_login->get_new_login_url(),
COOKIE_DOMAIN,
is_ssl(),
true
);
}
}
/**
* Removes extra cookies set during the password reset process.
*/
protected function remove_extra_cookies() {
if ( isset( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ) && 'flywheel' === Server::get_current_server() ) {
setcookie(
'wp-resetpass-' . COOKIEHASH,
'',
time() - YEAR_IN_SECONDS,
COOKIEPATH,
COOKIE_DOMAIN,
is_ssl(),
true
);
}
}
/**
* Force reset password.
*
* @param WP_User|WP_Error $user WP_User object or WP_Error.
* @param string $password Password plain string.
*
* @return WP_User|WP_Error Return user object or error object.
*/
public function do_force_reset( $user, $password ) {
if ( ! is_wp_error( $user ) && wp_check_password(
$password,
$user->user_pass,
$user->ID
) && $this->is_force_reset( $user ) ) {
$action = 'password_reset';
$cookie_name = 'display_reset_password_warning';
$this->trigger_redirect( $user, $action, $cookie_name );
}
return $user;
}
/**
* Reset weak password.
*
* @param WP_User|WP_Error $user WP_User object or WP_Error.
* @param string $password Password plain string.
*
* @return WP_User|WP_Error Return user object or error object.
*/
public function do_weak_reset( $user, $password ) {
if ( ! is_wp_error( $user ) && wp_check_password(
$password,
$user->user_pass,
$user->ID
) && $this->is_weak_password( $user, $password ) ) {
$action = 'password_protection';
$cookie_name = 'display_pwned_password_warning';
$this->trigger_redirect( $user, $action, $cookie_name );
}
return $user;
}
/**
* Redirect to reset password with error message.
*
* @param WP_User|WP_Error $user WP_User object or WP_Error.
* @param string $action Action query string name.
* @param string $cookie_name Cookie name.
*/
private function trigger_redirect( $user, $action, $cookie_name ) {
// Set cookie to check and display the warning notice on reset password page.
$this->set_cookie_notice( $cookie_name, true, time() + MINUTE_IN_SECONDS * 2 );
// Get the reset password URL.
$url = $this->get_reset_password_redirect_url( $user );
/**
* Fires before redirecting to the password reset page.
*
* @param string $url
* @param string $action
*
* @since 2.5.6
*/
do_action( 'wd_forced_reset_password_url', $url, $action );
// Redirect to the reset password page.
$this->reset_password_redirect( $url );
}
/**
* Verify if password is weak.
* Is user role enabled for weak password verification and is the password weak?
* If so then return true else password is strong therefore return false.
*
* @param WP_User $user WP_User object.
* @param string $password Plain password string.
*
* @return bool If password weak then true else false.
* @since 2.6.1
*/
public function is_weak_password( $user, $password ) {
$user_roles = $this->password_protection_model->user_roles;
$is_enabled_by_user = $this->is_enabled_by_user_role( $user, $user_roles );
$is_pwned = $this->check_pwned_password( $password );
$is_weak_password = $this->password_protection_model->is_active() && $is_enabled_by_user && ! is_wp_error( $is_pwned ) && $is_pwned;
return $is_weak_password;
}
/**
* Verify if password is need to be force reset.
* Is user role enabled for force password reset and is the password expired?
* If so then return true else password is non expired therefore return false.
*
* @param WP_User $user WP_User object.
*
* @return bool If password expired then true else false.
* @since 2.6.1
*/
public function is_force_reset( $user ) {
$user_roles = $this->model->user_roles;
$is_enabled_by_user = $this->is_enabled_by_user_role( $user, $user_roles );
$is_expired = $this->check_expired_password( $user );
$is_expired_password = $this->model->is_active() && $is_enabled_by_user && $is_expired;
return $is_expired_password;
}
}