CVE-2024-23830 (PHP, published 2024-02-20T22:15Z)
Description: value: MantisBT is an open source issue tracker. Prior to version 2.26.1, an unauthenticated attacker who knows a user's email address and username can hijack the user's account by poisoning the link in the password reset notification message. A patch is available in version 2.26.1. As a workaround, define `$g_path` as appropriate in `config_inc.php`.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/mantisbt/mantisbt
Files changed:
admin/check/check_paths_inc.php
admin/install.php
config_defaults_inc.php
core.php
docbook/Admin_Guide/en-US/config/path.xml
js/install.js
lang/strings_english.txt
login_page.php
Commit message: Merge pull request from GHSA-mcqj-7p29-9528

* Address host header injection vulnerability

$g_path is empty by default, and should be defined in config_inc.php.
Not doing so is a security risk, as the path will then be set based on
headers from the HTTP request, exposing the system to Host Header
injection attacks.

Document the risk in PHPDoc and Admin Guide.

Move the code that initializes $g_path's default value from
config_defaults_inc.php to a function in core.php.

Detect if $g_path was defaulted, and if yes alert the user in:
- Login Page (if $g_admin_checks == ON)
- Admin Checks

Fixes #19381, CVE-2024-23830, GHSA-mcqj-7p29-9528

* Remove dead code

* Use OWASP as reference for host header injection

* Link to OWASP reference page from admin guide

* Invalid $g_path at install time is a hard fail

Empty $g_path remains just a warning about the security risk.
Request and set $g_path at install time

This is an improvement on the original patch for CVE-2024-23830.

The admin is now able to set $g_path when installing MantisBT. A default
value is provided, based on the URL used to perform the installation
(using the same logic that is applied when $g_path is empty).

A check of the provided URL is performed during install stage 2, and an
error is reported if it is invalid. If an empty $g_path is given, then
we only display a warning about the security risk.

The URL is then stored as $g_path in the generated config_inc.php file
at stage 5.

This greatly reduces the risk of the admin forgetting to set $g_path
manually, while still allowing them to set it to empty should they want
to.

Fixes #19381

* Add Reset button to path input

Reuse the existing functionality implemented for database prefix/suffix,
with the following changes

- Rename `reset-prefix` selector class to `reset` to be more generic
- Add Reset functionality markup to path input including default value
- Add title attribute to Reset buttons
- Adapt initialization logic to only set the default value for the
  table-prefix fields

Vulnerable code:
		printf( '<input id="%1$s" name="%1$s" type="text" class="table-prefix" %2$s value="%3$s">',
		printf( '<button id="%s" type="button" class="btn btn-sm btn-primary btn-white btn-round reset-prefix">%s</button>',
$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( ["'", '"'], ['&#39;', '&#34;'], $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 /
$g_path	= $t_protocol . '://' . $t_host . $t_path;
 * Short web path without the domain name
 * requires trailing /
$g_short_path = $t_path;
# Temporary variables should not remain defined in global scope
unset( $t_protocol, $t_host, $t_hosts, $t_port, $t_self, $t_path );



				<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).
var reset_buttons = $('button.reset-prefix');
var inputs = $('input.table-prefix').each(function () {
	input.disableButton();
	var input = new PrefixInput($(this).prev('input.table-prefix')[0].id);
			. sprintf( lang_get( 'word_separator' ) )
			. sprintf( lang_get( "warning_{$p_type}_hazard" ) );

Fixed code:
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>.'
	)
);

	$f_path                   = config_get_global( 'path', '' );
	$f_path               = gpc_get( 'path', config_get_global( 'path', '' ) );
<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>
		printf( '<input id="%1$s" name="%1$s" type="text" class="table-prefix reset" %2$s value="%3$s">',
		printf( '<button id="%s" type="button" class="btn btn-sm btn-primary btn-white btn-round reset">%s</button>',

<!-- 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>

	if( $f_path ) {
		$t_config .= PHP_EOL
			. "\$g_path                   = '" . addslashes( $f_path ) . "';" . PHP_EOL;
	}

 * 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.
 *
$g_path	= '';
 * 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.
 *
$g_short_path = '';
# 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();

/**
 * 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( ["'", '"'], ['&#39;', '&#34;'], $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;
}

				<para>
					Full URL to your installation as seen from the web browser.
				<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>
var reset_buttons = $('button.reset').each( function () {
	this.title = "Reset the field to its default value";
});
$('input.table-prefix').each(function () {
});
var inputs = $('input.reset').each(function () {
	var input = new PrefixInput(this.id);
	if (input.isValueDefault()) {
		input.disableButton();
	}
	var input = new PrefixInput($(this).prev('input.reset')[0].id);
$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.';
	# $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' );
	}

			. lang_get( 'word_separator' )
			. lang_get( "warning_{$p_type}_hazard" );
		/** @var array $g_upgrade */