MantisBT is Vulnerable to Reflected XSS in Rendering Dynamic Custom Textarea Field
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.
| Package | Affected versions | Patched versions |
|---|---|---|
mantisbt/mantisbtPackagist | >= 1.0.0, < 2.28.2 | 2.28.2 |
Affected products
1Patches
1c885af13f0b8Fix XSS in return_dynamic_filters.php
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
4News mentions
0No linked articles in our index yet.