diff --git a/include/functions_mail.inc.php b/include/functions_mail.inc.php index 926246033..6eebe62ff 100644 --- a/include/functions_mail.inc.php +++ b/include/functions_mail.inc.php @@ -1104,6 +1104,46 @@ function pwg_generate_code_verification_mail($code) ); } +/** + * Generate content mail for reset password success + * + * Return the content mail to send + * @since 16 + * @param string $code + * @return array mail content + */ +function pwg_generate_success_reset_password_mail($username, $nb_of_apikeys) +{ + global $conf; + set_make_full_url(); + $profile_url = get_root_url().'profile.php'; + + $message = '
'.l10n('Hello %s,', $username).'
'; + $message .= ''.l10n('Your password was successfully reset').'.
'; + $message .= ''; + $message .= l10n('If this wasn\'t you, please change your password immediately or contact your webmaster.'); + $message .= '
'; + + if ($nb_of_apikeys > 0) + { + $message .= ''; + $message .= l10n( + 'If you changed your password because you think it was stolen, we recommend revoking your %d API keys in your profile.', + $nb_of_apikeys, + $profile_url + ); + $message .= '
'; + } + unset_make_full_url(); + + $subject = '['.$conf['gallery_title'].'] '.l10n('Your password has been reset'); + return array( + 'subject' => $subject, + 'content' => $message, + 'content_format' => 'text/html', + ); +} + trigger_notify('functions_mail_included'); ?> diff --git a/include/functions_user.inc.php b/include/functions_user.inc.php index 87c90c014..eae20fe68 100644 --- a/include/functions_user.inc.php +++ b/include/functions_user.inc.php @@ -2759,6 +2759,31 @@ SELECT return $api_keys; } +/** + * Get all available api_key + * + * @since 16 + * @param string $user_id + * @return array|false + */ +function get_available_api_key($user_id) +{ + $api_keys = get_api_key($user_id); + + if (!$api_keys) return false; + + $available = array(); + foreach($api_keys as $api_key) + { + if (!$api_key['is_expired'] && empty($api_key['revoked_on'])) + { + $available[] = $api_key; + } + } + + return count($available) > 0 ? $available : false; +} + /** * Is connected with pwg_ui (identification.php) * diff --git a/language/en_UK/common.lang.php b/language/en_UK/common.lang.php index b24c86a75..d8b751620 100644 --- a/language/en_UK/common.lang.php +++ b/language/en_UK/common.lang.php @@ -528,3 +528,9 @@ $lang['The secret will no longer be displayed. You must copy it to continue.'] = $lang['ID copied.'] = 'ID copied.'; $lang['Secret copied. Keep it in a safe place.'] = 'Secret copied. Keep it in a safe place.'; $lang['edit user preferences'] = 'edit user preferences'; +$lang['Here is your verification code:'] = 'Here is your verification code:'; +$lang['Your verification code'] = 'Your verification code'; +$lang['If this wasn\'t you, please change your password immediately or contact your webmaster.'] = 'If this wasn\'t you, please change your password immediately or contact your webmaster.'; +$lang['If you changed your password because you think it was stolen, we recommend revoking your %d API keys in your profile.'] = 'If you changed your password because you think it was stolen, we recommend revoking your %d API keys in your profile.'; +$lang['Too many attempts, please try later..'] = 'Too many attempts, please try later..'; +$lang['Verification successful! You can now choose a new password.'] = 'Verification successful! You can now choose a new password.'; diff --git a/language/fr_FR/common.lang.php b/language/fr_FR/common.lang.php index 7eeb0fc1a..0b2a74f05 100644 --- a/language/fr_FR/common.lang.php +++ b/language/fr_FR/common.lang.php @@ -526,4 +526,10 @@ $lang['Save your ID and secret'] = 'Enregistrez votre identifiant et votre secre $lang['The secret will no longer be displayed. You must copy it to continue.'] = 'Le secret ne sera plus affiché. Vous devez le copier pour continuer.'; $lang['ID copied.'] = 'Identifiant copié.'; $lang['Secret copied. Keep it in a safe place.'] = 'Secret copié. Gardez-le dans un endroit sûr.'; -$lang['edit user preferences'] = 'modifier les préférences utilisateur'; \ No newline at end of file +$lang['edit user preferences'] = 'modifier les préférences utilisateur'; +$lang['Here is your verification code:'] = 'Voici votre code de vérification :'; +$lang['Your verification code'] = 'Votre code de vérification'; +$lang['If this wasn\'t you, please change your password immediately or contact your webmaster.'] = 'Si ce n\'était pas vous, veuillez changer immédiatement votre mot de passe ou contacter votre webmaster.'; +$lang['If you changed your password because you think it was stolen, we recommend revoking your %d API keys in your profile.'] = 'Si vous avez changé votre mot de passe car vous pensez qu\'il a été volé, nous vous recommandons de révoquer vos %d clefs d\'API sur votre profil.'; +$lang['Too many attempts, please try later..'] = 'Trop de tentatives, veuillez réessayer plus tard..'; +$lang['Verification successful! You can now choose a new password.'] = 'Vérification réussie ! Vous pouvez maintenant choisir un nouveau mot de passe.'; diff --git a/password.php b/password.php index 0a9230379..5904c78b3 100644 --- a/password.php +++ b/password.php @@ -22,7 +22,7 @@ check_status(ACCESS_FREE); trigger_notify('loc_begin_password'); -check_input_parameter('action', $_GET, false, '/^(lost|reset|lost_code|lost_end|reset_end|none)$/'); +check_input_parameter('action', $_GET, false, '/^(lost|reset|lost_code|reset_end|none)$/'); // +-----------------------------------------------------------------------+ // | Functions | @@ -44,7 +44,7 @@ function process_verification_code() } // empty param - $username_or_email = trim($_POST['username_or_email']); + $username_or_email = trim($_POST['username_or_email'] ?? ''); if (empty($username_or_email)) { $page['errors']['password_form_error'] = l10n('Invalid username or email'); @@ -62,18 +62,40 @@ function process_verification_code() // when no user is found, we assign guest_id instead of stopping. // this lets the function behave identically for unknown users, // preventing username/email enumeration through timing or responses. - $is_user_founded = is_numeric($user_id); - if (!$is_user_founded) + $is_user_found = is_numeric($user_id); + if (!$is_user_found) { $user_id = $conf['guest_id']; } $userdata = getuserdata($user_id, false); + $status = $userdata['status']; + + if ($is_user_found) + { + // block early for generic or guest user because + // we don't consider theses users has sensible for username/email enumeration + if (is_a_guest($status) or is_generic($status)) + { + $page['errors']['password_form_error'] = l10n('Password reset is not allowed for this user'); + return false; + } + + // check lockout + if ( + isset($userdata['preferences']['reset_password_forbidden_until']) + and $userdata['preferences']['reset_password_forbidden_until'] > time() + ) + { + $page['errors']['password_form_error'] = l10n('Too many attempts, please try later..'); + return false; + } + } + // check if we want to skip email sending // if user is guest, generic or doesn't have email - $status = $userdata['status']; - $skip_mail = !$is_user_founded or is_a_guest($status) or is_generic($status) or empty($userdata['email']); + $skip_mail = !$is_user_found or empty($userdata['email']); // send mail with verification code to user switch_lang_to($userdata['language']); @@ -82,18 +104,13 @@ function process_verification_code() if (!$skip_mail) { $mail_send = pwg_mail($userdata['email'], $template_mail); - // pwg_activity('user', $userdata['id'], 'reset_password_code', array( - // 'ip' => $_SERVER['REMOTE_ADDR'], - // 'agent' => $_SERVER['HTTP_USER_AGENT'], - // 'is_mail_sent' => $mail_send - // )); } switch_lang_back(); $_SESSION['reset_password_code'] = [ 'secret' => $user_code['secret'], 'attempts' => 0, - 'user_id' => $is_user_founded ? $user_id : null, + 'user_id' => $is_user_found ? $user_id : null, 'created_at' => time(), 'ttl' => min($conf['password_reset_code_duration'], 900) // max 15 min ]; @@ -103,18 +120,18 @@ function process_verification_code() /** * checks the validity of input parameters, fills $page['errors'] and - * $page['infos'] and send an email with reset link + * $page['infos'] * - * @return bool (true if email was sent, false otherwise) + * @return bool (true if valid, false otherwise) */ function process_password_request() { - global $page, $conf; + global $page, $user; $state = $_SESSION['reset_password_code'] ?? null; if (!$state) { - return true; // fallback line 366 + return true; } // check expired @@ -143,10 +160,24 @@ function process_password_request() if ($_SESSION['reset_password_code']['attempts'] >= 3) { unset($_SESSION['reset_password_code']); - $page['errors']['login_page_error'] = l10n('Too many attempts'); + // lockout account for 1hour + if (!empty($state['user_id'])) + { + $save_user = $user; + $user = build_user($state['user_id'], false); + userprefs_update_param('reset_password_forbidden_until', time() + 60 * 60); + $user = $save_user; + + pwg_activity('user', $state['user_id'], 'reset_password_failure_too_many'); + } + $page['errors']['login_page_error'] = l10n('Too many attempts, please try later..'); return false; } + if (!empty($state['user_id'])) + { + pwg_activity('user', $state['user_id'], 'reset_password_failure_code'); + } $page['errors']['password_form_error'] = l10n('Invalid verification code'); return false; } @@ -161,28 +192,28 @@ function process_password_request() return false; } - $userdata = getuserdata($user_id); - $status = $userdata['status'] ?? null; + $save_user = $user; + $user = build_user($user_id, false); + userprefs_delete_param('reset_password_forbidden_until'); + + $_SESSION['valid_reset_password_code'] = array( + 'user_id' => $user_id, + 'username' => $user['username'], + 'email' => $user['email'], + 'language' => $user['language'], + ); + $status = $user['status'] ?? null; + $has_no_email = empty($user['email']); + $page['username'] = $user['username']; + $user = $save_user; // fallback check: don't send mail when user is guest, generic or doesn't have email - if (is_a_guest($status) || is_generic($status) || empty($userdata['email'])) + if (is_a_guest($status) || is_generic($status) || $has_no_email) { $page['errors']['password_form_error'] = l10n('Password reset is not allowed for this user'); return false; } - $generate_link = generate_password_link($user_id); - switch_lang_to($userdata['language']); - $email_params = pwg_generate_reset_password_mail($userdata['username'], $generate_link['password_link'], $conf['gallery_title'], $generate_link['time_validation']); - $send_email = pwg_mail($userdata['email'], $email_params); - switch_lang_back(); - - // pwg_activity('user', $userdata['id'], 'reset_password_link', array( - // 'ip' => $_SERVER['REMOTE_ADDR'], - // 'agent' => $_SERVER['HTTP_USER_AGENT'], - // 'is_mail_sent' => $send_email - // )); - return true; } @@ -253,15 +284,11 @@ function reset_password() return false; } - if (!isset($_GET['key'])) - { - $page['errors']['password_page_error'] = l10n('Invalid key'); - } - - $user_id = check_password_reset_key($_GET['key']); + $user_id = reset_password_key() ?: reset_password_code(); if (!is_numeric($user_id)) { + $page['errors']['password_form_error'] = l10n('Invalid key or code'); return false; } @@ -271,8 +298,21 @@ function reset_password() array($conf['user_fields']['id'] => $user_id) ); - deactivate_password_reset_key($user_id); - deactivate_user_auth_keys($user_id); + if (isset($_SESSION['valid_reset_password_code']) and !empty($_SESSION['valid_reset_password_code']['email'])) + { + $reset_user = $_SESSION['valid_reset_password_code']; + switch_lang_to($reset_user['language']); + + $api_keys = get_available_api_key($reset_user['user_id']); + $nb_of_apikeys = $api_keys ? count($api_keys) : 0; + $template_mail = pwg_generate_success_reset_password_mail($reset_user['username'], $nb_of_apikeys); + pwg_mail($reset_user['email'], $template_mail); + + switch_lang_back(); + } + unset($_SESSION['valid_reset_password_code']); + + pwg_activity('user', $user_id, 'reset_password_success'); $page['infos'][] = l10n('Your password has been reset'); $page['infos'][] = ''.l10n('Login').''; @@ -280,6 +320,35 @@ function reset_password() return true; } +function reset_password_key() +{ + if (!isset($_GET['key'])) + { + return false; + } + + $user_id = check_password_reset_key($_GET['key']); + + if (!is_numeric($user_id)) + { + return false; + } + + deactivate_password_reset_key($user_id); + deactivate_user_auth_keys($user_id); + return $user_id; +} + +function reset_password_code() +{ + if (!isset($_SESSION['valid_reset_password_code'])) + { + return false; + } + + return $_SESSION['valid_reset_password_code']['user_id'] ?? false; +} + // +-----------------------------------------------------------------------+ // | Process form | // +-----------------------------------------------------------------------+ @@ -300,8 +369,8 @@ if (isset($_POST['submit'])) { if (process_password_request()) { - $page['infos'][] = l10n('An email has been sent with a link to reset your password'); - $page['action'] = 'lost_end'; + $page['infos'][] = l10n('Verification successful! You can now choose a new password.'); + $page['action'] = 'reset'; } } @@ -358,9 +427,12 @@ if (!isset($page['action'])) } } -if ('reset' == $page['action'] and !isset($_GET['key']) and (is_a_guest() or is_generic())) +if ('reset' == $page['action']) { - redirect(get_gallery_home_url()); + if ( (!isset($_GET['key']) and (is_a_guest() or is_generic())) and !isset($_SESSION['valid_reset_password_code']) ) + { + redirect(get_gallery_home_url()); + } } if ('lost' == $page['action'] and !is_a_guest())