VYPR
High severity7.2GHSA Advisory· Published May 11, 2026· Updated May 11, 2026

MantisBT is Vulnerable to XSS leading to account takeover via updating a user's font family preference

CVE-2026-40596

Description

Any authenticated user can inject arbitrary HTML via updating their account's font family.

Impact

Cross-site scripting. The injected payload will be reflected in every MantisBT page.

Leveraging another vulnerability (CSP bypass, see GHSA-9c3j-xm6v-j7j3), the attacker could achieve account takeover.

### Patches - 9e8409cdd979eba86ef532756fc47c1d8112d22d

Workarounds

None

Credits

Thanks to siunam (Tang Cheuk Hei) for discovering and responsibly reporting the issue.

Affected products

1

Patches

1
9e8409cdd979

Merge branch 'sec-37011-xss_font_family' into release/2.28.2

https://github.com/mantisbt/mantisbtDamien RegadMay 6, 2026via ghsa
7 files changed · +58 22
  • account_prefs_update.php+19 13 modified
    @@ -36,6 +36,8 @@
      * @uses user_pref_api.php
      */
     
    +use Mantis\Exceptions\ClientException;
    +
     require_once( 'core.php' );
     require_api( 'access_api.php' );
     require_api( 'authentication_api.php' );
    @@ -79,14 +81,18 @@
     $t_prefs->refresh_delay		= gpc_get_int( 'refresh_delay' );
     $t_prefs->default_project	= gpc_get_int( 'default_project' );
     
    -$t_lang = gpc_get_string( 'language' );
    -if( lang_language_exists( $t_lang ) ) {
    -	$t_prefs->language = $t_lang;
    +$f_lang = gpc_get_string( 'language' );
    +if( lang_language_exists( $f_lang ) ) {
    +	$t_prefs->language = $f_lang;
     }
     
    -$t_font = gpc_get_string( 'font_family' );
    -if( config_get( 'font_family', null, $f_user_id, ALL_PROJECTS ) != $t_font ) {
    -	config_set( 'font_family', $t_font, $f_user_id, ALL_PROJECTS );
    +$f_font = gpc_get_string( 'font_family' );
    +if( !in_array( $f_font, helper_get_font_list())) {
    +	# This should not happen unless the form submission was tempered with
    +	throw new ClientException( "Invalid font", ERROR_INVALID_FIELD_VALUE, ['font_family'] );
    +}
    +if( config_get( 'font_family', null, $f_user_id, ALL_PROJECTS ) != $f_font ) {
    +	config_set( 'font_family', $f_font, $f_user_id, ALL_PROJECTS );
     }
     
     $t_prefs->email_on_new		= gpc_get_bool( 'email_on_new' );
    @@ -112,10 +118,10 @@
     $t_prefs->email_bugnote_limit = gpc_get_int( 'email_bugnote_limit' );
     
     # Save user preference with regards to getting full issue details in notifications or not.
    -$t_email_full_issue = gpc_get_bool( 'email_full_issue' ) ? 1 : 0;
    +$f_email_full_issue = gpc_get_bool( 'email_full_issue' ) ? 1 : 0;
     $t_email_full_config_option = 'email_notifications_verbose';
    -if( config_get( $t_email_full_config_option, /* default */ null, $f_user_id, ALL_PROJECTS ) != $t_email_full_issue ) {
    -	config_set( $t_email_full_config_option, $t_email_full_issue, $f_user_id, ALL_PROJECTS );
    +if( config_get( $t_email_full_config_option, /* default */ null, $f_user_id, ALL_PROJECTS ) != $f_email_full_issue ) {
    +	config_set( $t_email_full_config_option, $f_email_full_issue, $f_user_id, ALL_PROJECTS );
     }
     
     # make sure the delay isn't too low
    @@ -124,12 +130,12 @@
     	$t_prefs->refresh_delay = config_get( 'min_refresh_delay' );
     }
     
    -$t_timezone = gpc_get_string( 'timezone' );
    -if( in_array( $t_timezone, timezone_identifiers_list() ) ) {
    -	if( $t_timezone == config_get_global( 'default_timezone' ) ) {
    +$f_timezone = gpc_get_string( 'timezone' );
    +if( in_array( $f_timezone, timezone_identifiers_list() ) ) {
    +	if( $f_timezone == config_get_global( 'default_timezone' ) ) {
     		$t_prefs->timezone = '';
     	} else {
    -		$t_prefs->timezone = $t_timezone;
    +		$t_prefs->timezone = $f_timezone;
     	}
     }
     
    
  • core/helper_api.php+18 0 modified
    @@ -997,3 +997,21 @@ function helper_get_root_domain( $p_url ) {
     	}
     	return $t_host; // Return host if nothing matches
     }
    +
    +/**
    + * Returns the list of available font families.
    + *
    + * The list depends on whether CDN is enabled.
    + * @see $g_font_family_choices
    + * @see $g_font_family_choices_local
    + *
    + * @return array Font list, 0-based index.
    + */
    +function helper_get_font_list(): array {
    +	$t_config = config_get_global( 'cdn_enabled' ) == ON
    +		? 'font_family_choices'
    +		: 'font_family_choices_local';
    +	$t_font_list = config_get( $t_config );
    +
    +	return array_values( $t_font_list );
    +}
    
  • core/layout_api.php+1 0 modified
    @@ -314,6 +314,7 @@ function layout_head_css() {
      */
     function layout_user_font_preference() {
     	$t_font_family = config_get( 'font_family', null, null, ALL_PROJECTS );
    +	$t_font_family = string_html_specialchars( $t_font_family );
     	echo '<style>', "\n";
     	echo  '* { font-family: "' . $t_font_family . '"; } ', "\n";
     	echo  'h1, h2, h3, h4, h5 { font-family: "' . $t_font_family . '"; } ', "\n";
    
  • core/print_api.php+9 7 modified
    @@ -1196,14 +1196,16 @@ function print_language_option_list( $p_language ) {
      * @return void
      */
     function print_font_option_list( $p_font ) {
    -	if ( config_get_global( 'cdn_enabled' ) == ON ) {
    -		$t_arr = config_get( 'font_family_choices' );
    -	} else {
    -		$t_arr = config_get( 'font_family_choices_local' );
    +	$t_font_list = helper_get_font_list();
    +
    +	# Append given font to the list if it is not part of it
    +	if( !in_array( $p_font, $t_font_list ) ) {
    +		$t_font_list[] = $p_font;
    +		$p_font = string_html_specialchars( $p_font );
     	}
    -	$t_enum_count = count( $t_arr );
    -	for( $i = 0;$i < $t_enum_count;$i++ ) {
    -		$t_font = string_attribute( $t_arr[$i] );
    +
    +	foreach( $t_font_list as $t_key => $t_font ) {
    +		$t_font = string_html_specialchars( $t_font );
     		echo '<option value="' . $t_font . '"';
     		check_selected( $t_font, $p_font );
     		echo '>' . $t_font . '</option>';
    
  • login_page.php+2 1 modified
    @@ -226,7 +226,8 @@ function debug_setting_message ( $p_type, $p_setting, $p_value ) {
     				echo '<input type="hidden" name="install" value="true" />';
     			}
     
    -			# CSRF protection not required here - form does not result in modifications
    +			# Generating CSRF token to reduce risk of a vulnerability escalating its impact
    +			echo form_security_field( 'login' );
     			?>
     
     			<label for="username" class="block clearfix">
    
  • login_password_page.php+5 1 modified
    @@ -57,6 +57,8 @@
     require_api( 'utility_api.php' );
     require_css( 'login.css' );
     
    +form_security_validate( 'login' );
    +
     $f_error                 = gpc_get_bool( 'error' );
     $f_cookie_error          = gpc_get_bool( 'cookie_error' );
     $f_return                = string_sanitize_url( gpc_get_string( 'return', '' ) );
    @@ -219,7 +221,9 @@
     
     			echo sprintf( lang_get( 'enter_password' ), string_html_specialchars( $t_username ) );
     
    -			# CSRF protection not required here - form does not result in modifications
    +			# Generating CSRF token to reduce risk of a vulnerability escalating its impact
    +			form_security_purge( 'login' );
    +			echo form_security_field( 'login' );
     			?>
     			<input hidden readonly type="text" name="username" class="hidden" tabindex="-1" value="<?php echo string_html_specialchars( $t_username ) ?>" id="hidden_username" />
     			<label for="password" class="block clearfix">
    
  • login.php+4 0 modified
    @@ -43,6 +43,8 @@
     require_api( 'session_api.php' );
     require_api( 'string_api.php' );
     
    +form_security_validate( 'login' );
    +
     $f_username		= gpc_get_string( 'username', '' );
     $f_password		= gpc_get_string( 'password', '' );
     $t_return		= string_sanitize_url( gpc_get_string( 'return', config_get_global( 'default_home_page' ) ) );
    @@ -51,6 +53,8 @@
     $f_reauthenticate = gpc_get_bool( 'reauthenticate', false );
     $f_install = gpc_get_bool( 'install' );
     
    +form_security_purge( 'login' );
    +
     # If upgrade required, always redirect to install page.
     if( $f_install ) {
     	$t_return = 'admin/install.php';
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

6

News mentions

0

No linked articles in our index yet.