CVE-2024-23830: Insufficient Information (NVD-CWE-noinfo)
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`.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-02-20T22:15Z
Repository: https://github.com/mantisbt/mantisbt
Language: PHP
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 date: 2024-02-20 22:10:20 +0100
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
Diff: diff --git a/admin/check/check_paths_inc.php b/admin/check/check_paths_inc.php
index 394a25f4340..8afef332c3b 100644
--- a/admin/check/check_paths_inc.php
+++ b/admin/check/check_paths_inc.php
@@ -34,10 +34,22 @@ if( !defined( 'CHECK_PATHS_INC_ALLOW' ) ) {
 require_once( 'check_api.php' );
 require_api( 'config_api.php' );
 
 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',
 	'class_path',
 	'library_path',
diff --git a/admin/install.php b/admin/install.php
index 96e7b35cf86..8f0b4feff81 100644
--- a/admin/install.php
+++ b/admin/install.php
@@ -238,10 +238,11 @@ if( $t_config_exists && $t_install_state <= 1 ) {
 	$f_db_type                = config_get_global( 'db_type', '' );
 	$f_database_name          = config_get_global( 'database_name', '' );
 	$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';
 	foreach( $t_prefix_defaults[$t_prefix_type] as $t_key => $t_value ) {
 		${'f_' . $t_key} = $t_value;
@@ -256,10 +257,11 @@ if( $t_config_exists && $t_install_state <= 1 ) {
 	$f_db_password        = gpc_get( 'db_password', config_get_global( 'db_password', '' ) );
 	if( CONFIGURED_PASSWORD == $f_db_password ) {
 		$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';
 	foreach( $t_prefix_defaults[$t_prefix_type] as $t_key => $t_value ) {
 		${'f_' . $t_key} = gpc_get( $t_key, $t_value );
@@ -469,10 +471,44 @@ if( 2 == $t_install_state ) {
 		);
 		$t_version_info = null;
 	}
 	?>
 </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 ) {
 		?>
 <tr>
 	<td>
@@ -722,17 +758,17 @@ if( !$g_database_upgrade ) {
 		echo "<tr>\n\t<td>\n";
 		echo "\t\t" . $t_prefix_labels[$t_key] . "\n";
 		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&nbsp;";
-		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' )
 		);
 		echo "\n&nbsp;";
 		if( $t_key != 'db_table_suffix' ) {
@@ -763,10 +799,31 @@ if( !$g_database_upgrade ) {
 		<select id="timezone" name="timezone">
 			<?php print_timezone_option_list( $t_tz ) ?>
 		</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
 ?>
 
 <!-- Printing SQL queries -->
@@ -1322,10 +1379,15 @@ if( 5 == $t_install_state ) {
 		  '$g_default_timezone       = \'' . addslashes( $f_timezone ) . '\';' . PHP_EOL
 		. PHP_EOL
 		. (!$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 ) {
 		# Try to create the config file
 		if( is_writable( $g_config_path )
diff --git a/config_defaults_inc.php b/config_defaults_inc.php
index d0c9f7cc0d8..d02ce7decaa 100644
--- a/config_defaults_inc.php
+++ b/config_defaults_inc.php
@@ -192,87 +192,38 @@ unset( $t_local_config );
 
 ##########################
 # 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( ["'", '"'], ['&#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 /
+ * 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.
  * This can be either a full URL or a relative path to the MantisBT root.
  * If a relative path does not exist, the link will fall back to the online
@@ -5050,14 +5001,10 @@ $g_public_config_names = array(
 	'wiki_root_namespace',
 	'window_title',
 	'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 #
 ############################
 
 /**
diff --git a/core.php b/core.php
index bce4c79161e..f3ef7a5a715 100644
--- a/core.php
+++ b/core.php
@@ -97,15 +97,20 @@ if( file_exists( $g_config_path . 'custom_constants_inc.php' ) ) {
 	require_once( $g_config_path . 'custom_constants_inc.php' );
 }
 
 # 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 ) {
 	ensure_php_extension_loaded( 'ldap', 'when using LDAP as Login Method' );
 }
@@ -304,10 +309,93 @@ 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( ["'", '"'], ['&#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;
+}
+
 /**
  * Define an autoload function to automatically load classes when referenced
  *
  * @param string $p_class Class name being autoloaded.
  * @return void
diff --git a/docbook/Admin_Guide/en-US/config/path.xml b/docbook/Admin_Guide/en-US/config/path.xml
index 8f808de381b..c690eee0d14 100644
--- a/docbook/Admin_Guide/en-US/config/path.xml
+++ b/docbook/Admin_Guide/en-US/config/path.xml
@@ -11,17 +11,33 @@
 	</para>
 	<variablelist>
 		<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>
 			<term>$g_short_path</term>
 			<listitem>
diff --git a/js/install.js b/js/install.js
index ac108900fd0..289f7fbaf01 100644
--- a/js/install.js
+++ b/js/install.js
@@ -68,19 +68,26 @@ function PrefixInput (inputId) {
 		this.input.trigger('focus')[0].setSelectionRange(0, this.getValue().length);
 		this.disableButton();
 	};
 }
 
-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();
+	}
 });
 
 /**
  * On Change event for database type selection list
  * Preset prefix, plugin prefix and suffix fields when changing db type
@@ -131,11 +138,11 @@ 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();
 });
 
 update_sample_table_names();
diff --git a/lang/strings_english.txt b/lang/strings_english.txt
index b1da8923584..5db3fe4aee1 100644
--- a/lang/strings_english.txt
+++ b/lang/strings_english.txt
@@ -625,10 +625,11 @@ $s_please_report = 'Please report this to the %1$s.';
 
 # login.php
 $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.';
 $s_warning_debug_email = '<strong>Warning:</strong> "<code>debug_email</code>" is not set to <code>OFF</code>, all notification e-mails will be sent to "%1$s".';
 $s_error_database_no_schema_version = '<strong>Error:</strong> Your Database schema version is 0. This can happen either because the database is corrupt or the value of "database_version" could not be retrieved from the config table or the database structure is from an outdated version of MantisBT (pre 1.0.0).';
diff --git a/login_page.php b/login_page.php
index d3e7edc7773..fb6d1cc7b6a 100644
--- a/login_page.php
+++ b/login_page.php
@@ -150,21 +150,27 @@ if( config_get_global( 'admin_checks' ) == ON ) {
 		if( user_is_enabled( $t_admin_user_id ) && auth_does_password_match( $t_admin_user_id, 'root' ) ) {
 			$t_warnings[] = lang_get( 'warning_default_administrator_account_present' );
 		}
 	}
 
+	# $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.
 	 * @param string $p_setting Setting.
 	 * @param string $p_value   Value.
 	 * @return string
 	 */
 	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';
 	if( config_get_global( $t_config ) != OFF ) {
 		$t_warnings[] = debug_setting_message( 'security', $t_config, 'OFF' );
@@ -178,10 +184,11 @@ if( config_get_global( 'admin_checks' ) == ON ) {
 	}
 
 	# 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 ) &&
 			( $t_db_version != $t_upgrades_reqd ) ) {
 

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 */