CVE-2024-34077: Insufficient Information (NVD-CWE-noinfo)
value: MantisBT (Mantis Bug Tracker) is an open source issue tracker. Insufficient access control in the registration and password reset process allows an attacker to reset another user's password and takeover their account, if the victim has an incomplete request pending. The exploit is only possible while the verification token is valid, i.e for 5 minutes after the confirmation URL sent by e-mail has been opened, and the user did not complete the process by updating their password. A brute-force attack calling account_update.php with increasing user IDs is possible. A successful takeover would grant the attacker full access to the compromised account, including sensitive information and functionalities associated with the account, the extent of which depends on its privileges and the data it has access to. Version 2.26.2 contains a patch for the issue. As a workaround, one may mitigate the risk by reducing the verification token's validity (change the value of the `TOKEN_EXPIRY_AUTHENTICATED` constant in `constants_inc.php`).
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-14T15:38Z
Repository: https://github.com/mantisbt/mantisbt
Language: PHP
Files changed: account_update.php, verify.php
Commit date: 2024-05-12 17:40:28 +0200
Commit message: Validate confirm hash when updating account

This prevents an attacker from resetting another user's password if they
have an incomplete account validation or password reset request pending.

Fixes #34433, CVE-2024-xxxxx

Diff: diff --git a/account_update.php b/account_update.php
index 2cc20a8a747..8f66ace2004 100644
--- a/account_update.php
+++ b/account_update.php
@@ -58,23 +58,34 @@ require_api( 'string_api.php' );
 require_api( 'user_api.php' );
 require_api( 'utility_api.php' );
 
 form_security_validate( 'account_update' );
 
-# If token is set, it's a password reset request from verify.php, and if
-# not we need to reauthenticate the user
-$t_verify_user_id = gpc_get( 'verify_user_id', false );
-$t_account_verification = $t_verify_user_id ? token_get_value( TOKEN_ACCOUNT_VERIFY, $t_verify_user_id ) : false;
-if( !$t_account_verification ) {
-	auth_reauthenticate();
-	$t_user_id = auth_get_current_user_id();
-} else {
+$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 ) {
+		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 ) ) {
+		trigger_error( ERROR_SESSION_NOT_VALID, ERROR );
+	}
+
 	# set a temporary cookie so the login information is passed between pages.
 	auth_set_cookies( $t_verify_user_id );
 	# fake login so the user can set their password
 	auth_attempt_script_login( user_get_username( $t_verify_user_id ) );
 	$t_user_id = $t_verify_user_id;
+} else {
+	# Normal account update - authenticate the user
+	auth_reauthenticate();
+	$t_user_id = auth_get_current_user_id();
 }
 
 auth_ensure_user_authenticated();
 current_user_ensure_unprotected();
 
diff --git a/verify.php b/verify.php
index 2663251ccc2..3f997ce251b 100644
--- a/verify.php
+++ b/verify.php
@@ -129,10 +129,11 @@ layout_login_page_begin();
 			<form id="account-update-form" method="post" action="account_update.php">
 				<fieldset>
 					<legend><span><?php echo lang_get( 'edit_account_title' ) . ' - ' . string_display_line( $u_username ) ?></span></legend>
 					<div class="space-10"></div>
 					<input type="hidden" name="verify_user_id" value="<?php echo $u_id ?>">
+					<input type="hidden" name="confirm_hash" value="<?php echo string_html_specialchars( $f_confirm_hash ) ?>">
 					<?php
 					echo form_security_field( 'account_update' );
 					# When verifying account, set a token and don't display current password
 					token_set( TOKEN_ACCOUNT_VERIFY, true, TOKEN_EXPIRY_AUTHENTICATED, $u_id );
 					?>

Vulnerable code: # If token is set, it's a password reset request from verify.php, and if
# not we need to reauthenticate the user
$t_verify_user_id = gpc_get( 'verify_user_id', false );
$t_account_verification = $t_verify_user_id ? token_get_value( TOKEN_ACCOUNT_VERIFY, $t_verify_user_id ) : false;
if( !$t_account_verification ) {
	auth_reauthenticate();
	$t_user_id = auth_get_current_user_id();
} else {
Fixed code: $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 ) {
		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 ) ) {
		trigger_error( ERROR_SESSION_NOT_VALID, ERROR );
	}

} else {
	# Normal account update - authenticate the user
	auth_reauthenticate();
	$t_user_id = auth_get_current_user_id();
					<input type="hidden" name="confirm_hash" value="<?php echo string_html_specialchars( $f_confirm_hash ) ?>">