VYPR
Moderate severityGHSA Advisory· Published May 11, 2026· Updated May 11, 2026

MantisBT is Vulnerable to Reflected XSS in Rendering Dynamic Custom Textarea Field

CVE-2026-41897

Description

Lack of validation of filter_target parameter on return_dynamic_filters.php (normally used as an AJAX in View Issues Page) allows an attacker to inject arbitrary HTML if the target is a TEXTAREA custom field.

Impact

Cross-site scripting (XSS)

### Patches - c885af13f0b8596714ffe11df757c09f35fbd8f4

Workarounds

None

Credits

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

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

MantisBT lacks validation of the filter_target parameter in return_dynamic_filters.php, allowing reflected XSS via a crafted custom field ID that injects arbitrary HTML into a TEXTAREA.

Vulnerability

Overview

CVE-2026-2026-41897 is a reflected cross-site scripting (XSS) vulnerability in Mantis Bug Tracker (MantisBT). The issue resides in the return_dynamic_filters.php script, which is used as an AJAX endpoint on the View Issues page. The filter_target GET parameter is not properly validated or sanitized before being used to retrieve and render a custom field definition. Specifically, when the target is a TEXTAREA custom field, an attacker can inject arbitrary HTML by manipulating the custom field ID portion of the parameter [1][2].

Exploitation

Details

The vulnerable code path occurs when $f_filter_target starts with 'custom_field'custom_field_'. The script extracts the custom field ID by taking a substring starting at position 13 and removing the last 7 characters. This extracted value is passed to custom_field_get_definition(), which internally casts it to an integer via (int)$p_field_id. Because PHP's type casting truncates non-numeric prefixes, a value like 1foobar becomes the integer 1. If a custom field with ID 1 exists and the user has read access, the field is rendered via print_filter_custom_field(). The injected portion of the parameter (e.g., foobar) is not sanitized and can contain arbitrary HTML or JavaScript, leading to reflected XSS [2].

Impact

An attacker can exploit this vulnerability by crafting a malicious URL that, when visited by an authenticated user with access to the targeted custom field, executes arbitrary HTML/JavaScript in the context of the victim's browser. This could lead to session hijacking, defacement, or theft of sensitive information displayed on the page. The attack requires no special privileges beyond a valid user account that can view the custom field [1][2].

Mitigation

The vulnerability has been patched commit c885af13f0b8596714ffe11df757c09f35fbd8f4 addresses the issue by properly sanitizing the filter_target parameter and ensuring that the custom field ID is validated as an integer before use. Users should upgrade to a version containing this fix. No workarounds are available [1][4].

AI Insight generated on May 18, 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.

PackageAffected versionsPatched versions
mantisbt/mantisbtPackagist
>= 1.0.0, < 2.28.22.28.2

Affected products

1

Patches

1
c885af13f0b8

Fix XSS in return_dynamic_filters.php

https://github.com/mantisbt/mantisbtDamien RegadApr 19, 2026via ghsa
3 files changed · +40 36
  • core/date_api.php+26 32 modified
    @@ -303,46 +303,40 @@ function print_year_range_option_list( $p_year = 0, $p_start = 0, $p_end = 0 ) {
      * @access public
      */
     function print_date_selection_set( $p_name, $p_format, $p_date = 0, $p_default_disable = false, $p_allow_blank = false, $p_year_start = 0, $p_year_end = 0, $p_input_css = 'input-sm', $p_required = '' ) {
    -	$t_chars = preg_split( '//', $p_format, -1, PREG_SPLIT_NO_EMPTY );
    +	# Convert format to uppercase because we don't differentiate d/D, m/M, y/Y
    +	$t_chars = preg_split( '//', strtoupper( $p_format ),  -1, PREG_SPLIT_NO_EMPTY );
     	if( $p_date != 0 ) {
     		$t_date = preg_split( '/-/', date( 'Y-m-d', $p_date ), -1, PREG_SPLIT_NO_EMPTY );
     	} else {
     		$t_date = array( 0, 0, 0, );
     	}
     
    -	$t_disable = '';
    -	if( $p_default_disable == true ) {
    -		$t_disable = ' disabled="disabled"';
    -	}
    -	$t_blank_line = '';
    -	if( $p_allow_blank == true ) {
    -		$t_blank_line = '<option value="0"></option>';
    -	}
    +	$t_name = string_html_specialchars( $p_name );
    +	$t_disable = $p_default_disable ? 'disabled' : '';
    +	$t_blank_line = $p_allow_blank ?'<option value="0"></option>' : '';
    +	/** @noinspection HtmlUnknownAttribute */
    +	$t_template = <<<HTML
    +		<select name="%s" class="$p_input_css" %s $t_disable $p_required>
    +		$t_blank_line
    +		HTML;
     
     	foreach( $t_chars as $t_char ) {
    -		if( strcmp( $t_char, 'M' ) == 0 ) {
    -			echo '<select class="' . $p_input_css . '" ' . helper_get_tab_index() . ' name="' . $p_name . '_month"' . $t_disable . $p_required . '>';
    -			echo $t_blank_line;
    -			print_month_option_list( $t_date[1] );
    -			echo '</select>' . "\n";
    -		}
    -		if( strcmp( $t_char, 'm' ) == 0 ) {
    -			echo '<select class="' . $p_input_css . '" ' . helper_get_tab_index() . ' name="' . $p_name . '_month"' . $t_disable . $p_required . '>';
    -			echo $t_blank_line;
    -			print_month_option_list( $t_date[1] );
    -			echo '</select>' . "\n";
    -		}
    -		if( strcasecmp( $t_char, 'D' ) == 0 ) {
    -			echo '<select class="' . $p_input_css . '" ' . helper_get_tab_index() . ' name="' . $p_name . '_day"' . $t_disable . $p_required . '>';
    -			echo $t_blank_line;
    -			print_day_option_list( $t_date[2] );
    -			echo '</select>' . "\n";
    -		}
    -		if( strcasecmp( $t_char, 'Y' ) == 0 ) {
    -			echo '<select class="' . $p_input_css . '" ' .  helper_get_tab_index() . ' name="' . $p_name . '_year"' . $t_disable . $p_required . '>';
    -			echo $t_blank_line;
    -			print_year_range_option_list( $t_date[0], $p_year_start, $p_year_end );
    -			echo '</select>' . "\n";
    +		switch( $t_char ) {
    +			case 'Y':
    +				printf( $t_template, $t_name . '_year', helper_get_tab_index() );
    +				print_year_range_option_list( $t_date[0], $p_year_start, $p_year_end );
    +				echo "</select>\n";
    +				break;
    +			case 'M':
    +				printf( $t_template, $t_name . '_month', helper_get_tab_index() );
    +				print_month_option_list( $t_date[1] );
    +				echo "</select>\n";
    +				break;
    +			case 'D':
    +				printf( $t_template, $t_name . '_day', helper_get_tab_index() );
    +				print_day_option_list( $t_date[2] );
    +				echo "</select>\n";
    +				break;
     		}
     	}
     }
    
  • core/filter_form_api.php+7 3 modified
    @@ -2082,11 +2082,14 @@ function print_filter_custom_field( $p_field_id, ?array $p_filter = null ) {
     			break;
     
     		case CUSTOM_FIELD_TYPE_TEXTAREA:
    -			echo '<input class="input-xs" type="text" name="custom_field_', $p_field_id, '" size="10" value="" >';
    +			echo '<input class="input-xs" type="text" name="custom_field_',
    +				string_html_specialchars( $p_field_id ),
    +				'" size="10" value="" >';
     			break;
     
     		default:
    -			echo '<select class="input-xs" ' . filter_select_modifier( $p_filter ) . ' name="custom_field_' . $p_field_id . '[]">';
    +			echo '<select class="input-xs" ' . filter_select_modifier( $p_filter )
    +				. ' name="custom_field_' . string_html_specialchars( $p_field_id ) . '[]">';
     			# Option META_FILTER_ANY
     			echo '<option value="' . META_FILTER_ANY . '"';
     			check_selected( $p_filter['custom_fields'][$p_field_id], META_FILTER_ANY, false );
    @@ -2300,7 +2303,8 @@ function print_filter_custom_field_date( $p_field_id, ?array $p_filter = null )
     	}
     
     	echo '<table><tr><td>' . "\n";
    -	echo '<select class="input-xs" size="1" name="custom_field_' . $p_field_id . '_control">' . "\n";
    +	echo '<select class="input-xs" size="1" name="custom_field_'
    +		. string_html_specialchars( $p_field_id ) . '_control">' . "\n";
     	echo '<option value="' . CUSTOM_FIELD_DATE_ANY . '"';
     	check_selected( (int)$p_filter['custom_fields'][$p_field_id][0], CUSTOM_FIELD_DATE_ANY );
     	echo '>' . lang_get( 'any' ) . '</option>' . "\n";
    
  • return_dynamic_filters.php+7 1 modified
    @@ -35,8 +35,11 @@
      * @uses filter_form_api.php
      * @uses gpc_api.php
      * @uses helper_api.php
    - */
    + *
    + * @noinspection PhpUnhandledExceptionInspection
    + * */
     
    +use Mantis\Exceptions\ClientException;
     use Mantis\Exceptions\StateException;
     
     # Prevent output of HTML in the content if errors occur
    @@ -103,6 +106,9 @@ function return_dynamic_filters_prepend_headers() {
     } else if( 'custom_field' == mb_substr( $f_filter_target, 0, 12 ) ) {
     	# Check existence of custom field id, and if the user has access to read and filter by
     	$t_custom_id = mb_substr( $f_filter_target, 13, -7 );
    +	if( !is_numeric( $t_custom_id) ) {
    +		throw new ClientException( "Invalid custom field id", ERROR_CUSTOM_FIELD_NOT_FOUND );
    +	}
     	$t_cfdef = custom_field_get_definition( $t_custom_id );
     	if( $t_cfdef && access_has_any_project_level( $t_cfdef['access_level_r'] ) && $t_cfdef['filter_by'] ) {
     		$t_found = true;
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

4

News mentions

0

No linked articles in our index yet.