Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 220 additions & 28 deletions inc/spbc-auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
use CleantalkSP\SpbctWP\Counters\SecurityCounter;
use CleantalkSP\SpbctWP\Firewall\WafBlocker;
use CleantalkSP\SpbctWP\G2FA\GoogleAuthenticator;
use CleantalkSP\SpbctWP\UsersPassCheckModule\UsersPassCheckModel;
use CleantalkSP\SpbctWP\Variables\Cookie;
use CleantalkSP\SpbctWP\Helpers\IP;
use CleantalkSP\Variables\Get;
Expand All @@ -18,7 +17,7 @@
die('Not allowed!');
}

add_filter('authenticate', 'spbc_authenticate', 20, 3); // Hooks for authentificate
add_filter('authenticate', 'spbc_authenticate', 20, 2); // Hooks for authentificate

// Hook for token-based logins (plugins like "Temporary Login Without Password")
add_action('set_logged_in_cookie', 'spbc_detect_token_login', 10, 6);
Expand All @@ -30,6 +29,10 @@
add_action('profile_update', [UsersPassCheckHandler::class, 'removeUserPassOnPasswordChange'], 10, 1);
add_action('login_form', 'spbc_passleak_change_password_form', 10);
add_action('login_form_login', 'spbc_passleak_change_password_handler', 3);
// Clear flag on standard password reset
add_action('after_password_reset', 'spbc_passleak__clear_flag_on_reset', 10, 1);
// Global guards for forced password change
add_action('admin_init', 'spbc_passleak__force_password_change_guard', 1);
}

add_action('login_errors', 'spbc_fix_error_messages', 99999); // Filters error message
Expand Down Expand Up @@ -122,6 +125,7 @@ function spbc_login_form_notification()
*
* @param WP_User|WP_Error $user
* @param string $username
* @param string $password
*
* @return WP_Error|WP_User
*/
Expand Down Expand Up @@ -225,14 +229,10 @@ function spbc_authenticate($user, $username)
die();
}

// Redirect if password is leaked
// Set force password change flag if password is leaked
// Guard (spbc_passleak__force_password_change_guard) will redirect to password change form
if (UsersPassCheckHandler::isUserPassLeaked($user->ID)) {
wp_redirect(
wp_login_url()
. ( strpos(wp_login_url(), '?') === false ? '?' : '&' )
. 'spbc_passleak=' . rawurlencode($user->user_login)
);
die();
update_user_meta($user->ID, 'spbc_force_password_change', 1);
}

spbc_authenticate__write_log_login($user);
Expand Down Expand Up @@ -375,6 +375,12 @@ function spbc_detect_token_login($_logged_in_cookie, $_expire, $_expiration, $us
// Mark as logged to prevent any further duplicate logging
$spbc_login_logged = true;

// Check if password is leaked for token-based logins
if (UsersPassCheckHandler::isUserPassLeaked($user->ID)) {
// Set force password change flag
update_user_meta($user->ID, 'spbc_force_password_change', 1);
}

// Sends logs to get notify about superuser login.
$result = spbc_send_logs();
if (empty($result['error'])) {
Expand Down Expand Up @@ -540,29 +546,33 @@ function spbc_passleak_change_password_form()
{
global $spbc;

$user = null;
// Check if spbc_passleak parameter is present
$has_passleak_param = false;
if (isset($_GET['spbc_passleak'])) {
$user = $_GET['spbc_passleak'];
$has_passleak_param = true;
} else {
// Fallback to parsing REQUEST_URI if $_GET is empty because of .htaccess settings
$request_uri = filter_input(INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL);
if ($request_uri) {
$params = [];
parse_str(parse_url($request_uri, PHP_URL_QUERY) ?: '', $params);
$user = isset($params['spbc_passleak']) ? $params['spbc_passleak'] : null;
$has_passleak_param = isset($params['spbc_passleak']);
}
}

if (!$user) {
if (!$has_passleak_param) {
return;
}

$user_name = rawurldecode($user);
$user = spbc_get_user_by('login', $user_name);
// User must be logged in to see the password change form
$current_user = wp_get_current_user();
if (!$current_user || !$current_user->ID) {
return;
}

if (!$user) {
wp_redirect(home_url());
exit;
// Only show form if current user's password is actually leaked
if (!UsersPassCheckHandler::isUserPassLeaked($current_user->ID)) {
return;
}

// Error displaying
Expand All @@ -574,6 +584,9 @@ function spbc_passleak_change_password_form()
if ($_GET['spbc_passleak_error'] == '2') {
$err .= __('Current password is incorrect.', 'security-malware-firewall');
}
if ($_GET['spbc_passleak_error'] == '3') {
$err .= __('Too many attempts. Please, try again later.', 'security-malware-firewall');
}
echo sprintf(
'<script>var spbc_err = document.createElement("div");
spbc_err.innerHTML = \'<div id="login_error"><strong>%s</strong></div>\'
Expand All @@ -592,7 +605,6 @@ function spbc_passleak_change_password_form()
. '<input type="password" name="pass2" id="spbc_passleak_confirm" class="input" value="" size="20" />'
. '<label for="spbc_passleak_current">' . __('Your current password') . '</label>'
. '<input type="password" name="spbc_passleak_current" id="spbc_passleak_current" class="input" value="" size="20" />'
. '<input type="hidden" name="spbc_passleak_user" class="input" value="' . esc_attr($user_name) . '" />'
. '</p>'
. '<p class="submit" style="display: inline !important;">'
. '<div style="display: flex; justify-content: center; margin-top: 10px;">'
Expand All @@ -616,7 +628,6 @@ function spbc_passleak_change_password_form()

function spbc_passleak_change_password_handler()
{
$user_name = Post::getString('spbc_passleak_user');
$password_new = Post::getString('pass1');
$password_confirm = Post::getString('pass2');
$password_current = Post::getString('spbc_passleak_current');
Expand All @@ -628,31 +639,47 @@ function spbc_passleak_change_password_handler()
wp_die(__('Invalid nonce', 'security-malware-firewall'));
}

$user = spbc_get_user_by('login', $user_name);
if (!$user) {
wp_die(__('User not found', 'security-malware-firewall'));
// Rate limiting to prevent brute-force on password change form
if (!spbc_passleak__check_rate_limit()) {
wp_safe_redirect(
wp_login_url()
. ( strpos(wp_login_url(), '?') === false ? '?' : '&' )
. 'spbc_passleak=1'
. '&spbc_passleak_error=3'
);
die();
}

if ( ! UsersPassCheckModel::isUserPassLeaked($user->ID)) {
// User must be logged in
$user = wp_get_current_user();
if (!$user || !$user->ID) {
wp_safe_redirect(wp_login_url());
exit;
}

$user_name = $user->user_login;

if (!UsersPassCheckHandler::isUserPassLeaked($user->ID)) {
wp_safe_redirect(admin_url());
exit;
}

if ($password_new !== $password_confirm) {
wp_redirect(
wp_safe_redirect(
wp_login_url()
. ( strpos(wp_login_url(), '?') === false ? '?' : '&' )
. 'spbc_passleak=' . rawurlencode($user_name)
. 'spbc_passleak=1'
. '&spbc_passleak_error=1'
);
die();
}

// check if current password is correct
if (!wp_check_password($password_current, $user->user_pass)) {
wp_redirect(
wp_safe_redirect(
wp_login_url()
. ( strpos(wp_login_url(), '?') === false ? '?' : '&' )
. 'spbc_passleak=' . rawurlencode($user_name)
. 'spbc_passleak=1'
. '&spbc_passleak_error=2'
);
die();
Expand All @@ -662,11 +689,155 @@ function spbc_passleak_change_password_handler()
wp_set_password($password_new, $user->ID);
UsersPassCheckHandler::removeUserPassOnPasswordChange($user->ID);

// Clear force password change flag
spbc_passleak__clear_flag($user);

wp_signon([
'user_login' => $user_name,
'user_password' => $password_new
]);

$redirect_to = Post::getString('redirect_to') ?: admin_url();
wp_safe_redirect(
wp_sanitize_redirect($redirect_to)
);
exit;
}

/**
* Rate limiting for password change form.
* Uses bfp__allowed_wrong_auths setting for limit.
*
* @return bool True if rate limit is not exceeded
*/
function spbc_passleak__check_rate_limit()
{
global $spbc;

$ip = IP::get();
$transient_key = 'spbc_passleak_rl_' . md5($ip);
$time = time();
$limit = !empty($spbc->settings['bfp__allowed_wrong_auths'])
? (int) $spbc->settings['bfp__allowed_wrong_auths']
: 5;

$rateLimit = get_transient($transient_key);
if ($rateLimit === false) {
$rateLimit = [
'limit' => $limit,
'expires_in' => $time + 60,
'attempts' => 0,
];
}

// Update limit from settings in case it changed
$rateLimit['limit'] = $limit;

if ($rateLimit['expires_in'] <= $time) {
$rateLimit['expires_in'] = $time + 60;
$rateLimit['attempts'] = 0;
}

$rateLimit['attempts']++;

if ($rateLimit['attempts'] > $rateLimit['limit']) {
return false;
}

set_transient($transient_key, $rateLimit, 60);

return true;
}

/**
* Clear force password change flag for a user.
*
* @param WP_User|int $user User object or user ID
* @return void
*/
function spbc_passleak__clear_flag($user)
{
$user_id = is_object($user) ? $user->ID : (int) $user;
if ($user_id) {
delete_user_meta($user_id, 'spbc_force_password_change');
}
}

/**
* Clear force password change flag when user resets password via "Forgot Password".
*
* @param WP_User $user User object
* @return void
*/
function spbc_passleak__clear_flag_on_reset($user)
{
if ($user && $user->ID) {
spbc_passleak__clear_flag($user);
// Also remove from leaked passwords list since password is changing
UsersPassCheckHandler::removeUserPassOnPasswordChange($user->ID);
}
}

/**
* Global guard to force password change for users with leaked passwords.
* Hooks into admin_init and template_redirect.
* Allows only the password change form at wp-login.php?spbc_passleak=1
*
* @return void
*/
function spbc_passleak__force_password_change_guard()
{
// Skip if user is not logged in
if (!is_user_logged_in()) {
return;
}

$user = wp_get_current_user();
if (!$user || !$user->ID) {
return;
}

// Check if user has the force password change flag
$force_change = get_user_meta($user->ID, 'spbc_force_password_change', true);
if (empty($force_change)) {
return;
}

// Double-check if password is still leaked (flag might be stale)
if (!UsersPassCheckHandler::isUserPassLeaked($user->ID)) {
// Password is no longer leaked, clear the flag
spbc_passleak__clear_flag($user);
return;
}

// Allow wp-login.php?spbc_passleak=1 (password change form)
if (Server::inUri('wp-login.php')) {
// Check for spbc_passleak parameter
$has_passleak_param = false;
if (isset($_GET['spbc_passleak'])) {
$has_passleak_param = true;
} else {
// Fallback to parsing REQUEST_URI if $_GET is empty due to .htaccess settings
$request_uri = Server::getString('REQUEST_URI');
if ($request_uri) {
$params = [];
parse_str(parse_url($request_uri, PHP_URL_QUERY) ?: '', $params);
$has_passleak_param = isset($params['spbc_passleak']);
}
}

if ($has_passleak_param) {
return; // Allow access to password change form
}
}

// Redirect to password change form
wp_safe_redirect(
wp_login_url()
. (strpos(wp_login_url(), '?') === false ? '?' : '&')
. 'spbc_passleak=1'
);

$redirect_to = Post::getString('redirect_to') ?: admin_url();
wp_safe_redirect(
wp_sanitize_redirect($redirect_to)
Expand Down Expand Up @@ -926,6 +1097,27 @@ function spbc_2fa__success(\WP_User $user)
{
global $spbc;

// Check if password is leaked after 2FA success
if (UsersPassCheckHandler::isUserPassLeaked($user->ID)) {
// Set force password change flag
update_user_meta($user->ID, 'spbc_force_password_change', 1);

// Authorize user first
wp_set_auth_cookie($user->ID);
if (spbc_authenticate__is_new_device($user)) {
spbc_authenticate__browser_sign__set($user);
}
spbc_authenticate__user_agent__set($user);

// Redirect to password change form
wp_safe_redirect(
wp_login_url()
. ( strpos(wp_login_url(), '?') === false ? '?' : '&' )
. 'spbc_passleak=1'
);
die();
}

$type2fa = get_user_meta($user->ID, 'spbc_2fa_type', true);
$event = $type2fa === '2fa_application' ? 'login_g2fa' : 'login_2fa';

Expand Down
Loading