MantisBT: Authentication bypass for some passwords due to PHP type juggling
Description
Mantis Bug Tracker (MantisBT) is an open source issue tracker. In versions 2.27.1 and below, when a user edits their profile to change their e-mail address, the system saves it without validating that it actually belongs to the user. This could result in storing an invalid email address, preventing the user from receiving system notifications. Notifications sent to another person's email address could lead to information disclosure. This issue is fixed in version 2.27.2.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
MantisBT 2.27.1 and below does not verify email ownership when users change their email, allowing invalid addresses and potential information disclosure.
Vulnerability
Mantis Bug Tracker (MantisBT) versions 2.27.1 and below lack validation when a user changes their email address via the profile settings. The system saves the new email without confirming that the address belongs to the user [1]. This oversight allows users to set any arbitrary email address [2].
Exploitation
An authenticated user can simply edit their profile and enter any email address. No ownership verification is performed. The new address is stored immediately, and subsequent system notifications are sent to that address [1].
Impact
This can result in two issues: first, the user may enter an invalid or misspelled email, preventing them from receiving important notifications. Second, if the user enters an email belonging to another person, that person will receive notifications, potentially exposing sensitive information from the bug tracker [2].
Mitigation
The issue is fixed in MantisBT version 2.27.2. The fix introduces a new email verification process: after a user changes their email, a token is temporarily stored and a confirmation email is sent to the new address. The change only takes effect once the user clicks a verification link [4]. Users should upgrade to 2.27.2 or later.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
mantisbt/mantisbtPackagist | < 2.27.2 | 2.27.2 |
Affected products
2Patches
121e9fbedde85Verify new email after email change
16 files changed · +457 −83
account_page.php+41 −24 modified@@ -56,6 +56,9 @@ * @uses string_api.php * @uses user_api.php * @uses utility_api.php + * + * Unhandled exceptions will be caught by the default error handler + * @noinspection PhpUnhandledExceptionInspection */ require_once( 'core.php' ); @@ -71,6 +74,7 @@ require_api( 'ldap_api.php' ); require_api( 'print_api.php' ); require_api( 'string_api.php' ); +require_api( 'tokens_api.php' ); require_api( 'user_api.php' ); require_api( 'utility_api.php' ); @@ -179,30 +183,49 @@ ?> <tr> <td class="category"> - <span <?php echo $t_class . $t_required ?>><?php if( $t_force_pw_reset ) { ?> * <?php } ?></span> <?php echo lang_get( 'current_password' ) ?> + <label for="password-current"> + <span <?php echo $t_class . $t_required ?>><?php if( $t_force_pw_reset ) { ?> * <?php } ?></span> + <?php echo lang_get( 'current_password' ) ?> + </label> </td> <td> - <input class="input-sm" id="password-current" type="password" name="password_current" size="32" maxlength="<?php echo auth_get_password_max_size(); ?>" <?php echo $t_required ?> /> + <input id="password-current" name="password_current" type="password" class="input-sm" + size="32" maxlength="<?php echo auth_get_password_max_size(); ?>" + <?php echo $t_required ?> + /> </td> </tr> <tr> <td class="category"> - <span <?php echo $t_class . $t_required ?>><?php if( $t_force_pw_reset ) { ?> * <?php } ?></span> <?php echo lang_get( 'new_password' ) ?> + <label for="password"> + <span <?php echo $t_class . $t_required ?>><?php if( $t_force_pw_reset ) { ?> * <?php } ?></span> + <?php echo lang_get( 'new_password' ) ?> + </label> </td> <td> - <input class="input-sm" id="password" type="password" name="password" size="32" maxlength="<?php echo auth_get_password_max_size(); ?>" <?php echo $t_required ?> /> + <input id="password" name="password" type="password" class="input-sm" + size="32" maxlength="<?php echo auth_get_password_max_size(); ?>" + <?php echo $t_required ?> + /> </td> </tr> <tr> <td class="category"> - <span <?php echo $t_class . $t_required ?>><?php if( $t_force_pw_reset ) { ?> * <?php } ?></span> <?php echo lang_get( 'confirm_password' ) ?> + <label for="password-confirm"> + <span <?php echo $t_class . $t_required ?>><?php if( $t_force_pw_reset ) { ?> * <?php } ?></span> + <?php echo lang_get( 'confirm_password' ) ?> + </label> </td> <td> - <input class="input-sm" id="password-confirm" type="password" name="password_confirm" size="32" maxlength="<?php echo auth_get_password_max_size(); ?>" <?php echo $t_required ?> /> + <input id="password-confirm" name="password_confirm" type="password" class="input-sm" + size="32" maxlength="<?php echo auth_get_password_max_size(); ?>" + <?php echo $t_required ?> + /> </td> </tr> <?php } ?> + <tr> <td class="category"> <?php echo lang_get( 'email' ) ?> @@ -215,35 +238,29 @@ } else { # Without LDAP $t_show_update_button = true; + echo '<div class="">'; print_email_input( 'email', $u_email ); - if( config_get_global( 'email_ensure_unique' ) - && !user_is_email_unique( $u_email, $u_id ) - ) { - echo '<span class="padding-8">'; - print_icon('fa-exclamation-triangle', - 'ace-icon bigger-125 red padding-right-4' - ); - echo lang_get( 'email_not_unique' ); - echo '</span>'; - } + echo '</div>'; + print_email_not_unique_warning( $u_email, $u_id ); + print_email_pending_verification_warning( $u_id ); } ?> </td> </tr> <tr><?php + echo '<td class="category">' . lang_get( 'realname' ) . '</td>'; + echo '<td>'; if( $t_ldap && ON == config_get_global( 'use_ldap_realname' ) ) { # With LDAP - echo '<td class="category">' . lang_get( 'realname' ) . '</td>'; - echo '<td>'; echo string_display_line( ldap_realname_from_username( $u_username ) ); - echo '</td>'; } else { # Without LDAP $t_show_update_button = true; - echo '<td class="category">' . lang_get( 'realname' ) . '</td>'; - echo '<td>'; - echo '<input class="input-sm" id="realname" type="text" size="32" maxlength="' . DB_FIELD_SIZE_REALNAME . '" name="realname" value="' . string_attribute( $u_realname ) . '" />'; - echo '</td>'; - } ?> + echo '<input class="input-sm" id="realname" type="text" size="32" maxlength="' + . DB_FIELD_SIZE_REALNAME . '" name="realname" value="' + . string_attribute( $u_realname ) . '" />'; + } + echo '</td>'; + ?> </tr> <tr> <td class="category">
account_update.php+65 −7 modified@@ -36,6 +36,7 @@ * @uses lang_api.php * @uses print_api.php * @uses string_api.php + * @uses tokens_api.php * @uses user_api.php * @uses utility_api.php * @@ -55,23 +56,37 @@ require_api( 'lang_api.php' ); require_api( 'print_api.php' ); require_api( 'string_api.php' ); +require_api( 'tokens_api.php' ); require_api( 'user_api.php' ); require_api( 'utility_api.php' ); form_security_validate( 'account_update' ); +$f_verify_email = false; +$t_new_email = null; $t_verify_user_id = gpc_get_int( 'verify_user_id', 0 ); $t_account_verification = (bool)$t_verify_user_id; if( $t_account_verification ) { # Password reset request from verify.php - validate the confirmation hash $f_confirm_hash = gpc_get_string( 'confirm_hash' ); $t_token_confirm_hash = token_get_value( TOKEN_ACCOUNT_ACTIVATION, $t_verify_user_id ); - if( $t_token_confirm_hash == null || $f_confirm_hash !== $t_token_confirm_hash ) { + + # Email verification + $f_verify_email = gpc_get_bool( 'verify_email' ); + if( $f_verify_email ) { + $t_new_email = token_get_value( TOKEN_ACCOUNT_CHANGE_EMAIL, $t_verify_user_id ); + } + + if( $t_token_confirm_hash == null + || $f_confirm_hash !== $t_token_confirm_hash + || $f_verify_email && $t_new_email === null + ) { trigger_error( ERROR_LOST_PASSWORD_CONFIRM_HASH_INVALID, ERROR ); } - # Make sure the token is not expired - if( null === token_get_value( TOKEN_ACCOUNT_VERIFY, $t_verify_user_id ) ) { + # Make sure the token is not expired (except for email validation) + if( !$f_verify_email && + null === token_get_value( TOKEN_ACCOUNT_VERIFY, $t_verify_user_id ) ) { trigger_error( ERROR_SESSION_NOT_VALID, ERROR ); } @@ -89,6 +104,14 @@ auth_ensure_user_authenticated(); current_user_ensure_unprotected(); +if( $f_verify_email ) { + user_set_email( $t_user_id, $t_new_email ); + token_delete( TOKEN_ACCOUNT_CHANGE_EMAIL, $t_user_id ); + form_security_purge( 'account_update' ); + print_header_redirect( 'account_page.php' ); + exit(); +} + $f_email = gpc_get_string( 'email', '' ); $f_realname = gpc_get_string( 'realname', '' ); $f_password_current = gpc_get_string( 'password_current', '' ); @@ -112,8 +135,10 @@ # Update email (but only if LDAP isn't being used) # Do not update email for a user verification -if( !( $t_ldap && config_get_global( 'use_ldap_email' ) ) - && !$t_account_verification ) { +if( !$t_account_verification + && !( $t_ldap && config_get_global( 'use_ldap_email' ) ) +) { + $f_email = trim( $f_email ); if( !is_blank( $f_email ) && $f_email != user_get_email( $t_user_id ) ) { $t_update_email = true; } @@ -148,8 +173,26 @@ } } +# For security, email is only updated after the user has confirmed that they +# own the new address by clicking a verification link sent to them. +$t_show_confirmation_message = false; if( $t_update_email ) { - user_set_email( $t_user_id, $f_email ); + # Allow direct update if sending of reset email is disabled + if( !config_get( 'send_reset_password' ) ) { + user_set_email( $t_user_id, $f_email ); + } else { + user_ensure_email_valid( $t_user_id, $f_email ); + + # Temporarily store the new email address in a token + token_set( TOKEN_ACCOUNT_CHANGE_EMAIL, $f_email, TOKEN_EXPIRY_ACCOUNT_ACTIVATION, $t_user_id ); + + # Send verification mail + $t_confirm_hash = auth_generate_confirm_hash( $t_user_id ); + token_set( TOKEN_ACCOUNT_ACTIVATION, $t_confirm_hash, TOKEN_EXPIRY_ACCOUNT_ACTIVATION, $t_user_id ); + email_send_email_verification_url( $t_user_id, $t_confirm_hash, $f_email ); + + $t_show_confirmation_message = true; + } } if( $t_update_password ) { @@ -168,4 +211,19 @@ form_security_purge( 'account_update' ); -print_header_redirect( 'index.php' ); +if( $t_show_confirmation_message ) { + # Display confirmation message + layout_page_header(); + layout_page_begin(); + html_operation_successful( + "account_page.php", + '<p class="bold bigger-110">' . lang_get( 'operation_successful' ) . '</p><br>' + . sprintf( lang_get( 'verify_email_confirm_msg' ), $f_email + + ) + ); + layout_page_end(); + # Do not redirect +} else { + print_header_redirect( 'index.php' ); +}
api/soap/mc_account_api.php+5 −0 modified@@ -63,6 +63,11 @@ function mci_account_get_array_by_id( $p_user_id ) { if( !empty( $t_email ) ) { $t_result['email'] = $t_email; } + + $t_email_pending = token_get_value( TOKEN_ACCOUNT_CHANGE_EMAIL, $p_user_id ); + if( $t_email_pending !== null ) { + $t_result['email_pending'] = $t_email_pending; + } } } return $t_result;
core/commands/UserUpdateCommand.php+48 −25 modified@@ -194,10 +194,7 @@ function validate() { } if( !is_null( $t_new_email ) && $t_new_email !== $t_old_email ) { - email_ensure_valid( $t_new_email ); - email_ensure_not_disposable( $t_new_email ); - user_ensure_email_unique( $t_new_email, $this->user_id ); - + user_ensure_email_valid( $this->user_id, $t_new_email ); $this->email = $t_new_email; } @@ -268,7 +265,7 @@ function validate() { $this->old_user = array( 'id' => $this->user_id, 'username' => $t_old_username, - 'real_name' => $t_old_realname, + 'realname' => $t_old_realname, 'email' => $t_old_email, 'access_level' => $t_old_access_level, 'enabled' => $t_old_enabled, @@ -278,7 +275,7 @@ function validate() { $this->new_user = array( 'id' => $this->user_id, 'username' => $t_new_username ?: $t_old_username, - 'real_name' => !is_null( $t_new_realname ) ? $t_new_realname : $t_old_realname, + 'realname' => !is_null( $t_new_realname ) ? $t_new_realname : $t_old_realname, 'email' => $t_new_email ?: $t_old_email, 'access_level' => $t_new_access_level ?: $t_old_access_level, 'enabled' => !is_null( $t_new_enabled ) ? $t_new_enabled : $t_old_enabled, @@ -291,7 +288,7 @@ function validate() { * * @return array Command response * - * @throws ClientException + * @throws ClientException|\PHPMailer\PHPMailer\Exception */ protected function process() { $this->update_user( $this->new_user ); @@ -326,27 +323,53 @@ protected function process() { * authorization, triggering of events, etc. * * @param array $p_user User data + * * @return void + * @throws ClientException */ private function update_user( $p_user ) { - db_param_push(); - - $t_query = 'UPDATE {user} - SET username=' . db_param() . ', email=' . db_param() . ', - access_level=' . db_param() . ', enabled=' . db_param() . ', - protected=' . db_param() . ', realname=' . db_param() . ' - WHERE id=' . db_param(); - - $t_query_params = array( - $p_user['username'], - $p_user['email'], - $p_user['access_level'], - $p_user['enabled'], - $p_user['protected'], - $p_user['real_name'], - $p_user['id'] ); - - db_query( $t_query, $t_query_params ); + $t_user_id = array_shift( $p_user ); + + # Email was changed + if( $this->email ) { + # Change made by user themselves + if( auth_get_current_user_id() == $this->user_id ) { + if( config_get( 'send_reset_password' ) ) { + # Temporarily store the new email address in a token + token_set( TOKEN_ACCOUNT_CHANGE_EMAIL, + $this->email, + TOKEN_EXPIRY_ACCOUNT_ACTIVATION, + $t_user_id + ); + + # Send verification mail + $t_confirm_hash = auth_generate_confirm_hash( $this->user_id ); + token_set( TOKEN_ACCOUNT_ACTIVATION, + $t_confirm_hash, + TOKEN_EXPIRY_ACCOUNT_ACTIVATION, + $t_user_id + ); + email_send_email_verification_url( $this->user_id, $t_confirm_hash, $p_user['email'] ); + + # Do not update the user record + unset( $p_user['email'] ); + } + } else { + # Clear any pending change email token + token_delete( TOKEN_ACCOUNT_CHANGE_EMAIL, $this->user_id ); + } + } + + $t_query = new DbQuery( 'UPDATE {user} SET ' ); + $t_sql_columns = []; + foreach( $p_user as $t_col => $t_value ) { + $t_sql_columns[] = $t_col . ' = ' . $t_query->param( $t_value ); + } + $t_query->append_sql( + implode( ', ', $t_sql_columns ) + . ' WHERE id=' . $t_query->param( $t_user_id ) + ); + $t_query->execute(); } }
core/constant_inc.php+1 −0 modified@@ -535,6 +535,7 @@ define( 'TOKEN_COLLAPSE', 5 ); define( 'TOKEN_ACCOUNT_VERIFY', 6 ); define( 'TOKEN_ACCOUNT_ACTIVATION', 7 ); +define( 'TOKEN_ACCOUNT_CHANGE_EMAIL', 8 ); define( 'TOKEN_USER', 1000 ); # Token expiry durations (in seconds)
core/email_api.php+46 −2 modified@@ -539,8 +539,8 @@ function email_user_changed( $p_user_id, $p_old_user, $p_new_user ) { $t_changes .= lang_get( 'username_label' ) . ' ' . $p_old_user['username'] . ' => ' . $p_new_user['username'] . "\n"; } - if( strcmp( $p_old_user['real_name'], $p_new_user['real_name'] ) ) { - $t_changes .= lang_get( 'realname_label' ) . ' ' . $p_old_user['real_name'] . ' => ' . $p_new_user['real_name'] . "\n"; + if( strcmp( $p_old_user['realname'], $p_new_user['realname'] ) ) { + $t_changes .= lang_get( 'realname_label' ) . ' ' . $p_old_user['realname'] . ' => ' . $p_new_user['realname'] . "\n"; } if( strcmp( $p_old_user['email'], $p_new_user['email'] ) ) { @@ -669,6 +669,50 @@ function email_send_confirm_hash_url( $p_user_id, $p_confirm_hash, $p_reset_by_a lang_pop(); } +/** + * Send confirm_hash URL to let user validate a new email address. + * + * @param int $p_user_id A valid user identifier. + * @param string $p_confirm_hash Confirmation hash. + * @param string $p_new_email The new email address + * + * @return void + * @throws ClientException + */ +function email_send_email_verification_url( $p_user_id, $p_confirm_hash, $p_new_email ) { + # TODO is this needed ? + if( OFF == config_get( 'send_reset_password' ) ) { + log_event( LOG_EMAIL_VERBOSE, 'Password reset email notifications disabled.' ); + return; + } + + lang_push( user_pref_get_language( $p_user_id ) ); + + # retrieve the username and email + $t_username = user_get_username( $p_user_id ); + $t_old_email = user_get_email( $p_user_id ); + + $t_subject = '[' . config_get( 'window_title' ) . '] ' + . lang_get( 'verify_email_title' ); + + $t_message = lang_get( 'verify_email_msg' ) + . "\n\n" + . string_get_confirm_hash_url( $p_user_id, $p_confirm_hash, 'verify_email.php' ) + . "\n\n" + . lang_get( 'new_account_username' ) . ' ' . $t_username . "\n" + . lang_get( 'new_value' ) . ': ' . $p_new_email . "\n" + . lang_get( 'old_value' ) . ': ' . $t_old_email . "\n" + . lang_get( 'new_account_IP' ) . ' ' . $_SERVER['REMOTE_ADDR'] + . "\n\n" + . lang_get( 'new_account_do_not_reply' ); + + # Send regardless of mail notification preferences + email_store( $p_new_email, $t_subject, $t_message, null, true, [$t_old_email] ); + log_event( LOG_EMAIL, 'Email verification message for user @U%d sent to %s', $p_user_id, $p_new_email ); + + lang_pop(); +} + /** * Notify the selected group a new user has signup. *
core/print_api.php+40 −0 modified@@ -246,6 +246,46 @@ function print_email_input( $p_field_name, $p_email ) { echo '<input class="input-sm" id="email-field" type="text" name="' . string_attribute( $p_field_name ) . '" size="32" maxlength="64" value="' . string_attribute( $p_email ) . '" />'; } +/** + * Prints a warning message indicating that the email address is not unique. + * + * Nothing is printed if the email address is unique. + * + * @param string $p_email Email address to check + * @param int $p_user_id User Id + * + * @return void + */ +function print_email_not_unique_warning( string $p_email, int $p_user_id ): void { + if( config_get_global( 'email_ensure_unique' ) + && !user_is_email_unique( $p_email, $p_user_id ) + ) { + echo '<div class="padding-8">'; + print_icon( 'fa-exclamation-triangle', 'ace-icon bigger-125 red padding-right-4' ); + echo lang_get( 'email_not_unique' ); + echo '</div>'; + } +} + +/** + * Prints a warning message if the user's email address is pending validation. + * + * @param int $p_user_id User Id + * + * @return void + */ +function print_email_pending_verification_warning( int $p_user_id ): void { + # Get pending email address from token + $t_email_change = token_get_value( TOKEN_ACCOUNT_CHANGE_EMAIL, $p_user_id ); + + if( $t_email_change ) { + echo '<div class="padding-8">'; + print_icon('fa-info-circle', 'ace-icon bigger-125 blue padding-right-4' ); + printf( lang_get( 'verify_email_pending' ), $t_email_change ); + echo '</div>'; + } +} + /** * print out an email editing input *
core/string_api.php+9 −5 modified@@ -809,13 +809,17 @@ function string_get_bug_report_url() { } /** - * return the complete URL link to the verify page including the confirmation hash - * @param integer $p_user_id A valid user identifier. - * @param string $p_confirm_hash The confirmation hash value to include in the link. + * Return the complete URL link to the verify page including the confirmation hash. + * + * @param int $p_user_id A valid user identifier. + * @param string $p_confirm_hash The confirmation hash value to include in the link. + * @param string $p_page Verify Page (defaults to verify.php) + * * @return string */ -function string_get_confirm_hash_url( $p_user_id, $p_confirm_hash ) { - return config_get_global( 'path' ) . 'verify.php?id=' . string_url( $p_user_id ) . '&confirm_hash=' . string_url( $p_confirm_hash ); +function string_get_confirm_hash_url( $p_user_id, $p_confirm_hash, $p_page = 'verify.php' ) { + return config_get_global( 'path' ) . $p_page + . '?id=' . string_url( $p_user_id ) . '&confirm_hash=' . string_url( $p_confirm_hash ); } /**
core/tokens_api.php+15 −0 modified@@ -92,6 +92,21 @@ function token_get( $p_type, $p_user_id = null ) { return null; } +/** + * Get all tokens of a given type. + * + * @param int $p_type The token type to retrieve. + * + * @return array Token rows + */ +function token_get_by_type( int $p_type ) { + token_purge_expired_once(); + + $t_query = new DbQuery(); + $t_query->sql( 'SELECT * FROM {tokens} WHERE type=' . $t_query->param( $p_type ) ); + return $t_query->fetch_all(); +} + /** * Get a token's value or null if not found * @param integer $p_type The token type to retrieve.
core/user_api.php+19 −5 modified@@ -1904,8 +1904,10 @@ function user_set_password( $p_user_id, $p_password, $p_allow_protected = false # When the password is changed, invalidate the cookie to expire sessions that # may be active on all browsers. $c_cookie_string = auth_generate_unique_cookie_string(); + # Delete token for password activation if there is any token_delete( TOKEN_ACCOUNT_ACTIVATION, $p_user_id ); + token_delete( TOKEN_ACCOUNT_CHANGE_EMAIL, $p_user_id ); $c_password = auth_process_plain_password( $p_password ); @@ -1919,15 +1921,15 @@ function user_set_password( $p_user_id, $p_password, $p_allow_protected = false } /** - * Set the user's email after checking that it is valid. + * Validate the user's email address. * * @param int $p_user_id A valid user identifier. * @param string $p_email An email address to set. * - * @return bool - * @throws ClientException + * @return void + * @throws ClientException If mail is not valid. */ -function user_set_email( $p_user_id, $p_email ) { +function user_ensure_email_valid( $p_user_id, $p_email ) { $p_email = trim( $p_email ); email_ensure_valid( $p_email ); @@ -1937,8 +1939,20 @@ function user_set_email( $p_user_id, $p_email ) { if( strcasecmp( $t_old_email, $p_email ) != 0 ) { user_ensure_email_unique( $p_email ); } +} - return user_set_field( $p_user_id, 'email', $p_email ); +/** + * Set the user's email after checking that it is valid. + * + * @param int $p_user_id A valid user identifier. + * @param string $p_email An email address to set. + * + * @return void + * @throws ClientException + */ +function user_set_email( $p_user_id, $p_email ) { + user_ensure_email_valid( $p_user_id, $p_email ); + user_set_field( $p_user_id, 'email', $p_email ); } /**
lang/strings_english.txt+11 −1 modified@@ -280,6 +280,10 @@ $s_reset_request_admin_msg = 'Your password has been reset. Please visit the fol $s_reset_request_in_progress_msg = 'If you supplied the correct username and e-mail address for your account, we will now have sent a confirmation message to that e-mail address. Once the message has been received, follow the instructions provided to change the password on your account.'; +$s_verify_email_msg = 'Someone (presumably you) modified the email address associated with the account mentioned below. The old email address will remain active until you confirm this change. + +To validate the new email, please visit the following URL:'; + $s_email_notification_title_for_status_bug_new = 'The following issue is now in status NEW (again)'; $s_email_notification_title_for_status_bug_feedback = 'The following issue requires FEEDBACK.'; $s_email_notification_title_for_status_bug_acknowledged = 'The following issue has been ACKNOWLEDGED.'; @@ -657,6 +661,12 @@ $s_lost_password_subject = 'Password Reset'; $s_lost_password_info = 'To reinstate your lost password, please supply the name and e-mail address for the account.<br /><br />If the data corresponds to a valid account, you will be sent a special URL via e-mail that contains a validation code for your account. Please follow this link to change your password.'; $s_lost_password_confirm_hash_OK = 'Your confirmation has been accepted. Please update your password.'; +# verify_email.php +$s_verify_email_title = 'Verify Email Address'; +$s_verify_email_pending = 'New email address "%1$s" is pending verification.'; +$s_verify_email_warning = 'Your new email address "%1$s" has been verified.'; +$s_verify_email_confirm_msg = 'To validate the change of email address, a confirmation message has been sent to "%1$s". The old address will remain active until the new one has been verified.'; + # main_page.php $s_open_and_assigned_to_me_label = 'Open and assigned to me:'; $s_open_and_reported_to_me_label = 'Open and reported by me:'; @@ -1712,7 +1722,7 @@ $MANTIS_ERROR[ERROR_SIGNUP_NOT_MATCHING_CAPTCHA] = 'Confirmation hash does not m $MANTIS_ERROR[ERROR_LOST_PASSWORD_NOT_ENABLED] = 'The "lost your password" feature is not available.'; $MANTIS_ERROR[ERROR_LOST_PASSWORD_NO_EMAIL_SPECIFIED] = 'You must provide an e-mail address in order to reset the password.'; $MANTIS_ERROR[ERROR_LOST_PASSWORD_NOT_MATCHING_DATA] = 'The provided information does not match any registered account!'; -$MANTIS_ERROR[ERROR_LOST_PASSWORD_CONFIRM_HASH_INVALID] = 'The confirmation URL is invalid or has already been used. Please signup again.'; +$MANTIS_ERROR[ERROR_LOST_PASSWORD_CONFIRM_HASH_INVALID] = 'The confirmation URL is invalid or has already been used.'; $MANTIS_ERROR[ERROR_LOST_PASSWORD_MAX_IN_PROGRESS_ATTEMPTS_REACHED] = 'Maximum number of in-progress requests reached. Please contact the system administrator.'; $MANTIS_ERROR[ERROR_USER_CHANGE_LAST_ADMIN] = 'You cannot remove or demote the last administrator account. To perform the action you requested, you first need to create another administrator account.'; $MANTIS_ERROR[ERROR_PAGE_REDIRECTION] = 'Page redirection error, ensure that there are no spaces outside the PHP block (<?php ?>) in config_inc.php or custom_*.php files.';
lost_pwd.php+1 −0 modified@@ -96,6 +96,7 @@ $t_confirm_hash = auth_generate_confirm_hash( $t_user_id ); token_set( TOKEN_ACCOUNT_ACTIVATION, $t_confirm_hash, TOKEN_EXPIRY_ACCOUNT_ACTIVATION, $t_user_id ); +token_delete( TOKEN_ACCOUNT_CHANGE_EMAIL, $t_user_id ); email_send_confirm_hash_url( $t_user_id, $t_confirm_hash ); user_increment_lost_password_in_progress_count( $t_user_id );
manage_user_edit_page.php+2 −10 modified@@ -210,16 +210,8 @@ <td> <?php print_email_input( 'email', $t_user['email'] ); - if( config_get_global( 'email_ensure_unique' ) - && !user_is_email_unique( $t_user['email'], $t_user_id ) - ) { - echo '<span class="padding-8">'; - print_icon('fa-exclamation-triangle', - 'ace-icon bigger-125 red padding-right-4' - ); - echo lang_get( 'email_not_unique' ); - echo '</span>'; - } + print_email_not_unique_warning( $t_user['email'], $t_user_id ); + print_email_pending_verification_warning( $t_user_id ); ?> </td> <?php
manage_user_page.php+20 −1 modified@@ -35,6 +35,7 @@ * @uses lang_api.php * @uses print_api.php * @uses string_api.php + * @uses tokens_api.php * @uses utility_api.php */ @@ -51,6 +52,7 @@ require_api( 'lang_api.php' ); require_api( 'print_api.php' ); require_api( 'string_api.php' ); +require_api( 'tokens_api.php' ); require_api( 'utility_api.php' ); auth_reauthenticate(); @@ -382,6 +384,14 @@ $t_duplicate_emails = config_get_global( 'email_ensure_unique' ) ? user_get_duplicate_emails() : []; + + # User accounts with an email verification pending (user_id => new email) + $t_emails_pending_verification = token_get_by_type( TOKEN_ACCOUNT_CHANGE_EMAIL); + $t_emails_pending_verification = array_combine( + array_column( $t_emails_pending_verification, 'owner' ), + array_column( $t_emails_pending_verification, 'value' ) + ); + $t_access_level = array(); foreach( $t_users as $t_user ) { /** @@ -426,6 +436,15 @@ lang_get( 'email_not_unique' ) ); } + + # Display warning icon if email is pending verification + if( isset( $t_emails_pending_verification[$v_id] ) ) { + $t_msg = sprintf( lang_get( 'verify_email_pending' ), $t_emails_pending_verification[$v_id] ); + print_icon( 'fa-info-circle', + 'ace-icon bigger-125 blue padding-right-4', + string_html_specialchars( $t_msg ) + ); + } print_email_link( $v_email, $v_email ) ?></td> <td><?php echo $t_access_level[$v_access_level] ?></td> @@ -442,7 +461,7 @@ <td><?php echo $v_failed_login_count ?></td><?php } ?> </tr> <?php - } # end for + } # end foreach ?> </tbody> </table>
verify_email.php+127 −0 added@@ -0,0 +1,127 @@ +<?php +# MantisBT - A PHP based bugtracking system + +# MantisBT is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# MantisBT is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with MantisBT. If not, see <http://www.gnu.org/licenses/>. + +/** + * Verify Email Page + * + * @package MantisBT + * @copyright Copyright 2000 - 2002 Kenzaburo Ito - kenito@300baud.org + * @copyright Copyright 2002 MantisBT Team - mantisbt-dev@lists.sourceforge.net + * @link https://www.mantisbt.org + * + * @uses core.php + * @uses authentication_api.php + * @uses config_api.php + * @uses constant_inc.php + * @uses gpc_api.php + * @uses print_api.php + * @uses user_api.php + * + * Unhandled exceptions will be caught by the default error handler + * @noinspection PhpUnhandledExceptionInspection + */ + +# don't auto-login when trying to verify new user +$g_login_anonymous = false; + +require_once( 'core.php' ); +require_api( 'authentication_api.php' ); +require_api( 'config_api.php' ); +require_api( 'constant_inc.php' ); +require_api( 'gpc_api.php' ); +require_api( 'lang_api.php' ); +require_api( 'print_api.php' ); +require_api( 'string_api.php' ); +require_api( 'user_api.php' ); +require_api( 'tokens_api.php' ); +require_api( 'utility_api.php' ); +require_css( 'login.css' ); + + +$f_user_id = gpc_get_int( 'id' ); +$f_confirm_hash = gpc_get_string( 'confirm_hash' ); + +# Force logout on the current user if already authenticated +if( auth_is_user_authenticated() ) { + auth_logout(); + + # reload the page after logout + print_header_redirect( 'verify_email.php?id=' . $f_user_id . '&confirm_hash=' . $f_confirm_hash ); +} + +# Make sure the hash is valid and we're actually verifying an e-mail address +$t_token_confirm_hash = token_get_value( TOKEN_ACCOUNT_ACTIVATION, $f_user_id ); +$t_token_change_email = token_get_value( TOKEN_ACCOUNT_CHANGE_EMAIL, $f_user_id ); +if( $t_token_confirm_hash === null + || $t_token_change_email === null + || $f_confirm_hash !== $t_token_confirm_hash +) { + trigger_error( ERROR_LOST_PASSWORD_CONFIRM_HASH_INVALID, ERROR ); +} + +# Login again as the user +auth_attempt_script_login( user_get_username( $f_user_id ) ); +user_increment_login_count( $f_user_id ); +$t_row = user_get_row( $f_user_id ); +extract( $t_row, EXTR_PREFIX_ALL, 'u' ); + +$t_form_title = lang_get( 'verify_email_title' ); + +layout_login_page_begin( $t_form_title ); +?> + +<div class="col-md-offset-4 col-md-4 col-sm-8 col-sm-offset-1"> + <div class="login-container"> + <div class="space-12 hidden-480"></div> + <?php layout_login_page_logo() ?> + <div class="space-24 hidden-480"></div> + + <div class="position-relative"> + <div class="signup-box visible widget-box no-border" id="login-box"> + <div class="widget-body"> + <div class="widget-main"> + + <div id="verify-div" class="form-container"> + <form id="account-update-form" method="post" action="account_update.php"> + <legend> + <span><?php echo $t_form_title . ' - ' . string_display_line( $u_username ) ?></span> + </legend> + + <div id="reset-passwd-msg" class="alert alert-sm alert-warning "> + <?php printf( lang_get( 'verify_email_warning' ), $t_token_change_email ); ?> + </div> + + <input type="hidden" name="verify_user_id" value="<?php echo $u_id ?>"> + <input type="hidden" name="verify_email" value="1"> + <input type="hidden" name="confirm_hash" value="<?php echo string_html_specialchars( $f_confirm_hash ) ?>"> + <?php echo form_security_field( 'account_update' ); ?> + <div class="space-10"></div> + + <button type="submit" class="width-100 width-40 btn btn-success btn-inverse bigger-110"> + <span class="bigger-110"><?php echo lang_get( 'update_user_button' ) ?></span> + </button> + </form> + </div> + + </div> + </div> + </div> + </div> + </div> +</div> + +<?php +layout_login_page_end();
verify.php+7 −3 modified@@ -59,7 +59,7 @@ trigger_error( ERROR_LOST_PASSWORD_NOT_ENABLED, ERROR ); } -$f_user_id = gpc_get_string( 'id' ); +$f_user_id = gpc_get_int( 'id' ); $f_confirm_hash = gpc_get_string( 'confirm_hash' ); # force logout on the current user if already authenticated @@ -70,9 +70,13 @@ print_header_redirect( 'verify.php?id=' . $f_user_id . '&confirm_hash=' . $f_confirm_hash ); } +# Make sure the hash is valid and not meant for an e-mail address validation $t_token_confirm_hash = token_get_value( TOKEN_ACCOUNT_ACTIVATION, $f_user_id ); - -if( $t_token_confirm_hash == null || $f_confirm_hash !== $t_token_confirm_hash ) { +$t_token_change_email = token_get_value( TOKEN_ACCOUNT_CHANGE_EMAIL, $f_user_id ); +if( $t_token_confirm_hash == null + || $f_confirm_hash !== $t_token_confirm_hash + || $t_token_change_email !== null +) { trigger_error( ERROR_LOST_PASSWORD_CONFIRM_HASH_INVALID, ERROR ); }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-q747-c74m-69prghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-55155ghsaADVISORY
- github.com/mantisbt/mantisbt/commit/21e9fbedde8553c29c0d3156e84f78157fc4f22eghsax_refsource_MISCWEB
- github.com/mantisbt/mantisbt/security/advisories/GHSA-q747-c74m-69prghsax_refsource_CONFIRMWEB
- mantisbt.org/bugs/view.phpghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.