MantisBT Host Header Injection vulnerability
Description
MantisBT before 2.26.1 allows unauthenticated account hijack via password reset link poisoning using the HTTP Host header.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
MantisBT before 2.26.1 allows unauthenticated account hijack via password reset link poisoning using the HTTP Host header.
Vulnerability
Description
CVE-2024-23830 is a vulnerability in MantisBT, an open-source issue tracker, affecting versions prior to 2.26.1. The root cause is that the application dynamically determines the server path by using the HTTP_HOST header from the incoming HTTP request without proper validation. This allows an attacker to manipulate the host header and inject a malicious URL into the password reset email, poisoning the link that the user clicks to reset their password [1][2].
Exploitation
An unauthenticated attacker only needs to know the target user's email address and username. By crafting a request to the password reset functionality that includes a forged Host header pointing to an attacker-controlled server, the password reset link generated and sent via email will contain the attacker's domain. When the user clicks that link, they are taken to the attacker's site, which can serve a fake password reset form to capture the new password or directly hijack the session [2]. The attack does not require authentication or any special network position, as the host header can be manipulated from the client side [1].
Impact
Successful exploitation allows an attacker to completely hijack the user's account by obtaining the password reset token or tricking the user into entering credentials on a phishing page. This can lead to unauthorized access to sensitive issue tracker data and actions performed under the compromised user's identity [2].
Mitigation
The vulnerability is fixed in MantisBT version 2.26.1, which adds a configuration check to warn administrators if the $g_path is empty or improperly set, and allows setting a static path in config_inc.php as a workaround [3]. Administrators should update to the patched version or define $g_path in the configuration to avoid relying on the insecure dynamic detection [2].
AI Insight generated on May 20, 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.26.1 | 2.26.1 |
Affected products
2Patches
17055731d09ffMerge pull request from GHSA-mcqj-7p29-9528
8 files changed · +229 −89
admin/check/check_paths_inc.php+12 −0 modified@@ -36,6 +36,18 @@ check_print_section_header_row( 'Paths' ); +global $g_defaulted_path; +const HOST_HEADER_INJECTION_URL = 'https://owasp.org/www-project-web-security-testing-guide/stable/4-Web_Application_Security_Testing/07-Input_Validation_Testing/17-Testing_for_Host_Header_Injection'; +check_print_test_warn_row( + '"path" is defined in config_inc.php.', + !$g_defaulted_path, + array( false => + 'Leaving it empty is a security risk, as the path will be set based on ' + . 'headers from the HTTP request, exposing your system to ' + . '<a href="' . HOST_HEADER_INJECTION_URL . '">Host Header Injection attacks</a>.' + ) +); + $t_path_config_names = array( 'absolute_path', 'core_path',
admin/install.php+64 −2 modified@@ -240,6 +240,7 @@ function_exists( 'mysqli_stmt_get_result' ), $f_db_username = config_get_global( 'db_username', '' ); $f_db_password = config_get_global( 'db_password', '' ); $f_timezone = config_get( 'default_timezone', '' ); + $f_path = config_get_global( 'path', '' ); # Set default prefix/suffix form variables ($f_db_table_XXX) $t_prefix_type = 'other'; @@ -258,6 +259,7 @@ function_exists( 'mysqli_stmt_get_result' ), $f_db_password = config_get_global( 'db_password' ); } $f_timezone = gpc_get( 'timezone', config_get( 'default_timezone' ) ); + $f_path = gpc_get( 'path', config_get_global( 'path', '' ) ); # Set default prefix/suffix form variables ($f_db_table_XXX) $t_prefix_type = $f_db_type == 'oci8' ? $f_db_type : 'other'; @@ -471,6 +473,40 @@ function_exists( 'mysqli_stmt_get_result' ), } ?> </tr> +<tr> + <td> + Checking URL to installation + </td> + <?php + $t_url_check = ''; + if( !$f_path ) { + # Empty URL - warn admin about security risk + $t_url_check = "Using an empty path is a security risk, as MantisBT " + . "will dynamically set it based on headers from the HTTP request, " + . "exposing your system to Host Header Injection attacks."; + $t_hard_fail = false; + } else { + # Make sure we have a trailing '/' + $f_path = rtrim( $f_path, '/' ) . '/'; + + # Check that the URL is valid + if( !filter_var( $f_path, FILTER_VALIDATE_URL ) ) { + $t_url_check = "'$f_path' is not a valid URL."; + } else { + require_api( 'url_api.php' ); + $t_page_contents = url_get( $f_path ); + if( !$t_page_contents ) { + $t_url_check = "Can't retrieve web page at '$f_path'."; + } elseif( false === strpos( $t_page_contents, 'MantisBT') ) { + $t_url_check = "Web page at '$f_path' does not appear to be a MantisBT site."; + } + } + $t_hard_fail = true; + } + + print_test_result( $t_url_check ? BAD : GOOD, $t_hard_fail, $t_url_check ); + ?> +</tr> <?php if( $f_db_exists ) { ?> @@ -724,13 +760,13 @@ function_exists( 'mysqli_stmt_get_result' ), echo "\t</td>\n\t<td>\n\t\t"; $t_required = $t_key == 'db_table_plugin_prefix' ? 'required' : ''; /** @noinspection HtmlUnknownAttribute */ - printf( '<input id="%1$s" name="%1$s" type="text" class="table-prefix" %2$s value="%3$s">', + printf( '<input id="%1$s" name="%1$s" type="text" class="table-prefix reset" %2$s value="%3$s">', $t_key, $t_key == 'db_table_plugin_prefix' ? 'required' : '', ${'f_' . $t_key} // The actual value of the corresponding form variable ); echo "\n "; - printf( '<button id="%s" type="button" class="btn btn-sm btn-primary btn-white btn-round reset-prefix">%s</button>', + printf( '<button id="%s" type="button" class="btn btn-sm btn-primary btn-white btn-round reset">%s</button>', "btn_$t_key", lang_get( 'reset' ) ); @@ -765,6 +801,27 @@ function_exists( 'mysqli_stmt_get_result' ), </select> </td> </tr> + +<!-- URL to installation ($g_path) --> +<tr> + <td> + <label for="path">URL to your installation (<em>$g_path</em>, see + <a href="https://mantisbt.org/docs/master/en-US/Admin_Guide/html-desktop/#admin.config.path" + target="_blank">documentation</a>) + </label> + </td> + <td> + <input id="path" name="path" type="text" size="50" class="reset" + value="<?php echo string_attribute( $f_path ) ?>" + data-defval="<?php global $g_path; echo string_attribute( $g_path ); ?>" + /> + + <button id="btn_path" type="button" class="btn btn-sm btn-primary btn-white btn-round reset"> + <?php echo lang_get( 'reset' ); ?> + </button> + </td> +</tr> + <?php } # end install-only fields ?> @@ -1324,6 +1381,11 @@ function_exists( 'mysqli_stmt_get_result' ), . (!$t_crypto_master_salt ? "# The installer could not generate the Master Salt; please set it manually.\n" : '') . "\$g_crypto_master_salt = '" . addslashes( $t_crypto_master_salt ) . "';" . PHP_EOL; + if( $f_path ) { + $t_config .= PHP_EOL + . "\$g_path = '" . addslashes( $f_path ) . "';" . PHP_EOL; + } + $t_write_failed = true; if( !$t_config_exists ) {
config_defaults_inc.php+21 −74 modified@@ -194,83 +194,34 @@ # MantisBT Path Settings # ########################## -$t_protocol = 'http'; -$t_host = 'localhost'; -if( isset ( $_SERVER['SCRIPT_NAME'] ) ) { - $t_protocol = http_is_protocol_https() ? 'https' : 'http'; - - # $_SERVER['SERVER_PORT'] is not defined in case of php-cgi.exe - if( isset( $_SERVER['SERVER_PORT'] ) ) { - $t_port = ':' . $_SERVER['SERVER_PORT']; - if( ( ':80' == $t_port && 'http' == $t_protocol ) - || ( ':443' == $t_port && 'https' == $t_protocol )) { - $t_port = ''; - } - } else { - $t_port = ''; - } - - if( isset( $_SERVER['HTTP_X_FORWARDED_HOST'] ) ) { # Support ProxyPass - $t_hosts = explode( ',', $_SERVER['HTTP_X_FORWARDED_HOST'] ); - $t_host = $t_hosts[0]; - } else if( isset( $_SERVER['HTTP_HOST'] ) ) { - $t_host = $_SERVER['HTTP_HOST']; - } else if( isset( $_SERVER['SERVER_NAME'] ) ) { - $t_host = $_SERVER['SERVER_NAME'] . $t_port; - } else if( isset( $_SERVER['SERVER_ADDR'] ) ) { - $t_host = $_SERVER['SERVER_ADDR'] . $t_port; - } - - if( !isset( $_SERVER['SCRIPT_NAME'] )) { - echo 'Invalid server configuration detected. Please set $g_path manually in ' . $g_config_path . 'config_inc.php.'; - if( isset( $_SERVER['SERVER_SOFTWARE'] ) && ( stripos($_SERVER['SERVER_SOFTWARE'], 'nginx') !== false ) ) - echo ' Please try to add "fastcgi_param SCRIPT_NAME $fastcgi_script_name;" to the nginx server configuration.'; - die; - } - - # Prevent XSS if the path is displayed later on. This is the equivalent of - # FILTER_SANITIZE_STRING, which was deprecated in PHP 8.1: - # strip tags and null bytes, then encode quotes into HTML entities - $t_path = preg_replace( '/\x00|<[^>]*>?/', '', $_SERVER['SCRIPT_NAME'] ); - $t_path = str_replace( ["'", '"'], [''', '"'], $t_path ); - - $t_path = dirname( $t_path ); - switch( basename( $t_path ) ) { - case 'admin': - $t_path = dirname( $t_path ); - break; - case 'check': # admin checks dir - case 'soap': - case 'rest': - $t_path = dirname( $t_path, 2 ); - break; - case 'swagger': - $t_path = dirname( $t_path, 3 ); - break; - } - $t_path = rtrim( $t_path, '/\\' ) . '/'; - - if( strpos( $t_path, '&#' ) ) { - echo 'Can not safely determine $g_path. Please set $g_path manually in ' . $g_config_path . 'config_inc.php'; - die; - } -} else { - $t_path = 'mantisbt/'; -} - /** - * Path to your installation as seen from the web browser - * requires trailing / + * Full URL to your installation as seen from the web browser. + * + * Requires trailing `/`. + * + * If not set, MantisBT will default this to a working URL valid for most + * installations. + * + * WARNING: The default is built based on headers from the HTTP request + * ({@see set_default_path()} in core.php). This is a potential security risk, + * as the system will be exposed to Host Header injection attacks, so it is + * strongly recommended to initialize this in config_inc.php. + * * @global string $g_path */ -$g_path = $t_protocol . '://' . $t_host . $t_path; +$g_path = ''; /** - * Short web path without the domain name - * requires trailing / + * Short web path without the domain name. + * + * requires trailing `/`. + * + * This is defined by MantisBT core based on the script being executed, and + * should not be set in config_inc.php. + * * @global string $g_short_path */ -$g_short_path = $t_path; +$g_short_path = ''; /** * Used to link to manual for User Documentation. @@ -5052,10 +5003,6 @@ 'wrap_in_preformatted_text' ); -# Temporary variables should not remain defined in global scope -unset( $t_protocol, $t_host, $t_hosts, $t_port, $t_self, $t_path ); - - ############################ # Webservice Configuration # ############################
core.php+89 −1 modified@@ -99,11 +99,16 @@ # config_inc may not be present if this is a new install $t_config_inc_found = file_exists( $g_config_path . 'config_inc.php' ); - if( $t_config_inc_found ) { require_once( $g_config_path . 'config_inc.php' ); } +# Set global path variables +# NOTE: We store whether $g_path was defaulted, so we can later warn the admin +# about the exposure to host header injection attacks if they didn't set it. +global $g_defaulted_path; +$g_defaulted_path = set_default_path(); + # Ensure PHP LDAP extension is available when Login Method is LDAP global $g_login_method; if ( $g_login_method == LDAP ) { @@ -306,6 +311,89 @@ function http_is_protocol_https() { return false; } +/** + * Set Global Path variables. + * + * @see $g_path + * @see $g_short_path + * + * @return bool True if default $g_path was assigned, false if it was already set. + */ +function set_default_path() { + global $g_path, $g_short_path, $g_config_path; + + # $g_path is set in config_inc.php + if( $g_path ) { + # Derive $g_short_path from $g_path if not set + if( !$g_short_path ) { + $g_short_path = parse_url( $g_path, PHP_URL_PATH ); + } + return false; + } + + $t_protocol = 'http'; + $t_host = 'localhost'; + if( isset( $_SERVER['SCRIPT_NAME'] ) ) { + $t_protocol = http_is_protocol_https() ? 'https' : 'http'; + + # $_SERVER['SERVER_PORT'] is not defined in case of php-cgi.exe + if( isset( $_SERVER['SERVER_PORT'] ) ) { + $t_port = ':' . $_SERVER['SERVER_PORT']; + if( ( ':80' == $t_port && 'http' == $t_protocol ) + || ( ':443' == $t_port && 'https' == $t_protocol )) { + $t_port = ''; + } + } else { + $t_port = ''; + } + + if( isset( $_SERVER['HTTP_X_FORWARDED_HOST'] ) ) { # Support ProxyPass + $t_hosts = explode( ',', $_SERVER['HTTP_X_FORWARDED_HOST'] ); + $t_host = $t_hosts[0]; + } else if( isset( $_SERVER['HTTP_HOST'] ) ) { + $t_host = $_SERVER['HTTP_HOST']; + } else if( isset( $_SERVER['SERVER_NAME'] ) ) { + $t_host = $_SERVER['SERVER_NAME'] . $t_port; + } else if( isset( $_SERVER['SERVER_ADDR'] ) ) { + $t_host = $_SERVER['SERVER_ADDR'] . $t_port; + } + + # Prevent XSS if the path is displayed later on. This is the equivalent of + # FILTER_SANITIZE_STRING, which was deprecated in PHP 8.1: + # strip tags and null bytes, then encode quotes into HTML entities + $t_path = preg_replace( '/\x00|<[^>]*>?/', '', $_SERVER['SCRIPT_NAME'] ); + $t_path = str_replace( ["'", '"'], [''', '"'], $t_path ); + + $t_path = dirname( $t_path ); + switch( basename( $t_path ) ) { + case 'admin': + $t_path = dirname( $t_path ); + break; + case 'check': # admin checks dir + case 'soap': + case 'rest': + $t_path = dirname( $t_path, 2 ); + break; + case 'swagger': + $t_path = dirname( $t_path, 3 ); + break; + } + $t_path = rtrim( $t_path, '/\\' ) . '/'; + + if( strpos( $t_path, '&#' ) ) { + echo 'Can not safely determine $g_path. Please set $g_path manually in ' . $g_config_path . 'config_inc.php'; + die; + } + } else { + $t_path = 'mantisbt/'; + } + + $g_path = $t_protocol . '://' . $t_host . $t_path; + $g_short_path = $t_path; + + return true; +} + /** * Define an autoload function to automatically load classes when referenced *
docbook/Admin_Guide/en-US/config/path.xml+22 −6 modified@@ -13,13 +13,29 @@ <varlistentry> <term>$g_path</term> <listitem> - <para>URL to your installation as seen from the web browser; this - is what you type into the URL field. Requires trailing '/' - character. eg. 'https://www.example.com/mantisbt/'. MantisBT will default this to - the correct value. However, in some cases it might be necessary to - override the default. This is typically needed when an installation can be - accessed by multiple URLs (internal vs external). + <para> + Full URL to your installation as seen from the web browser. </para> + <para> + This is what users type into the URL field, e.g. + <literal>https://www.example.com/mantisbt/</literal>. + Requires trailing `/`. + </para> + <para> + If not set, MantisBT will default this to a working URL valid + for most installations. However, in some cases (typically when + an installation can be accessed by multiple URLs, e.g. internal + vs external), it might be necessary to override the default. + </para> + <warning><para> + The default is built based on headers from the HTTP request. + This is a potential security risk, as the system will be exposed to + <ulink url="https://owasp.org/www-project-web-security-testing-guide/stable/4-Web_Application_Security_Testing/07-Input_Validation_Testing/17-Testing_for_Host_Header_Injection" + >Host Header injection</ulink> + attacks, so it is + strongly recommended to initialize this in config_inc.php. + </para> + </warning> </listitem> </varlistentry> <varlistentry>
js/install.js+11 −4 modified@@ -70,15 +70,22 @@ function PrefixInput (inputId) { }; } -var reset_buttons = $('button.reset-prefix'); +var reset_buttons = $('button.reset').each( function () { + this.title = "Reset the field to its default value"; +}); /** * Initialize all input's default values and disable the reset buttons */ -var inputs = $('input.table-prefix').each(function () { +$('input.table-prefix').each(function () { var input = new PrefixInput(this.id); input.setDefault(input.getValue()); - input.disableButton(); +}); +var inputs = $('input.reset').each(function () { + var input = new PrefixInput(this.id); + if (input.isValueDefault()) { + input.disableButton(); + } }); /** @@ -133,7 +140,7 @@ inputs.on('input', function () { * Buttons to reset the prefix/suffix to the current default value */ reset_buttons.on('click', function () { - var input = new PrefixInput($(this).prev('input.table-prefix')[0].id); + var input = new PrefixInput($(this).prev('input.reset')[0].id); input.resetValue(); update_sample_table_names(); });
lang/strings_english.txt+1 −0 modified@@ -627,6 +627,7 @@ $s_please_report = 'Please report this to the %1$s.'; $s_warning_plain_password_authentication = '<strong>Warning:</strong> Plain password authentication is used, this will expose your passwords to administrators.'; $s_warning_default_administrator_account_present = '<strong>Warning:</strong> You should disable the default \'administrator\' account or change its password.'; $s_warning_admin_directory_present = '<strong>Warning:</strong> "admin" directory should be removed, or access to it restricted.'; +$s_warning_host_header_injection_hazard = '<strong>Warning:</strong> "path" was not defined in config_inc.php. Leaving it empty is a security risk, as the path will be set based on headers from the HTTP request, exposing your system to Host Header Injection attacks.'; $s_warning_change_setting = '<strong>Warning:</strong> "%1$s" is not set to its default value (%2$s).'; $s_warning_security_hazard = 'This is a potential security hazard as it can expose sensitive information.'; $s_warning_integrity_hazard = 'This will cause MantisBT to continue when errors occurs and may lead to system/data integrity issues.';
login_page.php+9 −2 modified@@ -152,6 +152,12 @@ } } + # $g_path was defaulted - risk of Host Header injection attack + global $g_defaulted_path; + if( $g_defaulted_path ) { + $t_warnings[] = lang_get( 'warning_host_header_injection_hazard' ); + } + /** * Display Warnings for enabled debugging / developer settings * @param string $p_type Message Type. @@ -161,8 +167,8 @@ */ function debug_setting_message ( $p_type, $p_setting, $p_value ) { return sprintf( lang_get( 'warning_change_setting' ), $p_setting, $p_value ) - . sprintf( lang_get( 'word_separator' ) ) - . sprintf( lang_get( "warning_{$p_type}_hazard" ) ); + . lang_get( 'word_separator' ) + . lang_get( "warning_{$p_type}_hazard" ); } $t_config = 'show_detailed_errors'; @@ -180,6 +186,7 @@ function debug_setting_message ( $p_type, $p_setting, $p_value ) { # Check for db upgrade for versions > 1.0.0 using new installer and schema if( $t_admin_dir_is_accessible ) { require_once( 'admin/schema.php' ); + /** @var array $g_upgrade */ $t_upgrades_reqd = count( $g_upgrade ) - 1; if( ( 0 < $t_db_version ) &&
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-mcqj-7p29-9528ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-23830ghsaADVISORY
- github.com/mantisbt/mantisbt/commit/7055731d09ff12b2781410a372f790172e279744ghsax_refsource_MISCWEB
- github.com/mantisbt/mantisbt/security/advisories/GHSA-mcqj-7p29-9528ghsax_refsource_CONFIRMWEB
- mantisbt.org/bugs/view.phpghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.