CVE-2021-32663: Server-Side Request Forgery (SSRF) (CWE-918)
value: iTop is an open source web based IT Service Management tool. In affected versions an attacker can call the system setup without authentication. Given specific parameters this can lead to SSRF. This issue has been resolved in versions 2.6.5 and 2.7.5 and later
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 7.5
Published: 2021-10-19T18:15Z
Repository: https://github.com/Combodo/iTop
Language: PHP
Files changed: setup/ajax.dataloader.php, setup/index.php, setup/setuputils.class.inc.php, setup/wizardcontroller.class.inc.php, setup/wizardsteps.class.inc.php
Commit date: 2021-05-27 09:29:50 +0200
Commit message: N°3952 - code hardening

Diff: diff --git a/setup/ajax.dataloader.php b/setup/ajax.dataloader.php
index 12787341..fdc18911 100644
--- a/setup/ajax.dataloader.php
+++ b/setup/ajax.dataloader.php
@@ -145,16 +145,11 @@ header("Expires: Fri, 17 Jul 1970 05:00:00 GMT");    // Date in the past
  * Main program
  */
 $sOperation = Utils::ReadParam('operation', '');
 try
 {
-	$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
-	if (!file_exists(APPROOT.'data/setup/authent') || $sAuthent !== file_get_contents(APPROOT.'data/setup/authent'))
-	{
-		throw new SecurityException('Setup operations are not allowed outside of the setup');
-		SetupPage::log_error("Setup operations are not allowed outside of the setup");
-	}
+	SetupUtils::CheckSetupToken();
 
 	switch($sOperation)
 	{
 		case 'async_action':
 		ini_set('max_execution_time', max(240, ini_get('max_execution_time')));
@@ -197,19 +192,18 @@ try
 }
 catch(Exception $e)
 {
 	header("HTTP/1.0 500 Internal server error.");
 	echo "<p>An error happened while processing the installation:</p>\n";
-	echo '<p>'.$e."</p>\n";
 	SetupPage::log_error("An error happened while processing the installation: ".$e);
 }
 
 if (function_exists('memory_get_peak_usage'))
 {
 	if ($sOperation == 'file')
 	{
-		SetupPage::log_info("loading file '$sFileName', peak memory usage. ".memory_get_peak_usage());
+		SetupPage::log_info("loading file peak memory usage. ".memory_get_peak_usage());
 	}
 	else
 	{
 		SetupPage::log_info("operation '$sOperation', peak memory usage. ".memory_get_peak_usage());
 	}
diff --git a/setup/index.php b/setup/index.php
index cc2cae1a..ea08bb78 100644
--- a/setup/index.php
+++ b/setup/index.php
@@ -28,10 +28,11 @@ require_once(APPROOT.'/application/utils.inc.php');
 require_once(APPROOT.'/core/config.class.inc.php');
 require_once(APPROOT.'/setup/setuppage.class.inc.php');
 require_once(APPROOT.'/setup/wizardcontroller.class.inc.php');
 require_once(APPROOT.'/setup/wizardsteps.class.inc.php');
 
+session_start();
 clearstatcache(); // Make sure we know what we are doing !
 // Set a long (at least 4 minutes) execution time for the setup to avoid timeouts during this phase
 ini_set('max_execution_time', max(240, ini_get('max_execution_time')));
 // While running the setup it is desirable to see any error that may happen
 ini_set('display_errors', true);
@@ -39,23 +40,29 @@ ini_set('display_startup_errors', true);
 date_default_timezone_set('Europe/Paris'); // Just to avoid a warning if the timezone is not set in php.ini
 
 /////////////////////////////////////////////////////////////////////
 // Fake functions to protect the first run of the installer
 // in case the PHP JSON module is not installed...
-if (!function_exists('json_encode'))
-{
+if (!function_exists('json_encode')) {
 	function json_encode($value, $options = null)
 	{
 		return '[]';
 	}
 }
-if (!function_exists('json_decode'))
-{
-	function json_decode($json, $assoc=null)
+if (!function_exists('json_decode')) {
+	function json_decode($json, $assoc = null)
 	{
 		return array();
 	}
 }
 /////////////////////////////////////////////////////////////////////
 
 $oWizard = new WizardController('WizStepWelcome');
-$oWizard->Run();
+//N°3952
+if (SetupUtils::IsSessionSetupTokenValid()) {
+	// Normal operation
+	$oWizard->Run();
+} else {
+	// Force initializing the setup
+	$oWizard->Start();
+	SetupUtils::CreateSetupToken();
+}
diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php
index 7b918abb..9ba7c82e 100644
--- a/setup/setuputils.class.inc.php
+++ b/setup/setuputils.class.inc.php
@@ -1824,10 +1824,79 @@ EOF
 	 */
 	static public function GetSetupQueriesFilePath()
 	{
 		return APPROOT.'log/setup-queries-'.strftime('%Y-%m-%d_%H_%M').'.sql';
 	}
+
+	/**
+	 * Create and store Setup authentication token
+	 *
+	 * @return string token
+	 */
+	public final static function CreateSetupToken()
+	{
+		if (!is_dir(APPROOT.'data'))
+		{
+			mkdir(APPROOT.'data');
+		}
+		if (!is_dir(APPROOT.'data/setup'))
+		{
+			mkdir(APPROOT.'data/setup');
+		}
+		$sUID = hash('sha256', rand());
+		file_put_contents(APPROOT.'data/setup/authent', $sUID);
+		$_SESSION['setup_token'] = $sUID;
+		return $sUID;
+	}
+
+	/**
+	 * Verify Setup authentication token (from the request parameter 'authent')
+	 *
+	 * @param bool $bRemoveToken
+	 *
+	 * @throws \SecurityException
+	 */
+	public final static function CheckSetupToken($bRemoveToken = false)
+	{
+		$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
+		$sTokenFile = APPROOT.'data/setup/authent';
+		if (!file_exists($sTokenFile) || $sAuthent !== file_get_contents($sTokenFile))
+		{
+			throw new SecurityException('Setup operations are not allowed outside of the setup');
+		}
+		if ($bRemoveToken)
+		{
+			@unlink($sTokenFile);
+		}
+	}
+
+	/**
+	 * Check setup transaction and create a new one if necessary
+	 *
+	 * @return bool
+	 */
+	public static function IsSessionSetupTokenValid()
+	{
+		if (isset($_SESSION['setup_token'])) {
+			$sAuth = $_SESSION['setup_token'];
+			$sTokenFile = APPROOT.'data/setup/authent';
+			if (file_exists($sTokenFile) && $sAuth === file_get_contents($sTokenFile)) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	public static function EraseSetupToken()
+	{
+		$sTokenFile = APPROOT.'data/setup/authent';
+		if (is_file($sTokenFile)) {
+			unlink($sTokenFile);
+		}
+		unset($_SESSION['setup_token']);
+	}
 }
 
 /**
  * Helper class to write rules (as PHP expressions) in the 'auto_select' field of the 'module'
  */
diff --git a/setup/wizardcontroller.class.inc.php b/setup/wizardcontroller.class.inc.php
index ac991e4c..377092be 100644
--- a/setup/wizardcontroller.class.inc.php
+++ b/setup/wizardcontroller.class.inc.php
@@ -103,11 +103,11 @@ class WizardController
 	}
 	
 	/**
 	 * Starts the wizard by displaying it in its initial state
 	 */
-	protected function Start()
+	public function Start()
 	{
 		$sCurrentStepClass = $this->sInitialStepClass;
 		$oStep = new $sCurrentStepClass($this, $this->sInitialState);
 		$this->DisplayStep($oStep);
 	}
@@ -119,11 +119,11 @@ class WizardController
 	{
 		$sCurrentStepClass = utils::ReadParam('_class', $this->sInitialStepClass);
 		$sCurrentState = utils::ReadParam('_state', $this->sInitialState);
 		/** @var \WizardStep $oStep */
 		$oStep = new $sCurrentStepClass($this, $sCurrentState);
-		if ($oStep->ValidateParams($sCurrentState))
+		if ($oStep->ValidateParams())
 		{
 			$this->PushStep(array('class' => $sCurrentStepClass, 'state' => $sCurrentState));
 			$aPossibleSteps = $oStep->GetPossibleSteps();
 			$aNextStepInfo = $oStep->ProcessParams(true); // true => moving forward
 			if (in_array($aNextStepInfo['class'], $aPossibleSteps))
@@ -171,18 +171,20 @@ class WizardController
 			if (file_exists($sConfigFile))
 			{
 				// The configuration file already exists
 				if (!is_writable($sConfigFile))
 				{
+					SetupUtils::EraseSetupToken();
 					$sRelativePath = utils::GetConfigFilePathRelative();
 					$oP = new SetupPage('Installation Cannot Continue');
 					$oP->add("<h2>Fatal error</h2>\n");
 					$oP->error("<b>Error:</b> the configuration file '".$sRelativePath."' already exists and cannot be overwritten.");
 					$oP->p("The wizard cannot modify the configuration file for you. If you want to upgrade ".ITOP_APPLICATION.", make sure that the file '<b>".$sRelativePath."</b>' can be modified by the web server.");
 					$oP->p('<button type="button" onclick="window.location.reload()">Reload</button>');
 					$oP->output();
-					return;
+					// Prevent token creation
+					exit;
 				}
 			}			
 		}
 		$oPage->add_linked_script('../setup/setup.js');
 		$oPage->add_script("function CanMoveForward()\n{\n".$oStep->JSCanMoveForward()."\n}\n");
diff --git a/setup/wizardsteps.class.inc.php b/setup/wizardsteps.class.inc.php
index 3efdddd6..2936be7f 100644
--- a/setup/wizardsteps.class.inc.php
+++ b/setup/wizardsteps.class.inc.php
@@ -55,20 +55,11 @@ class WizStepWelcome extends WizardStep
 		return array('WizStepInstallOrUpgrade');
 	}
 
 	public function ProcessParams($bMoveForward = true)
 	{
-		if (!is_dir(APPROOT.'data'))
-		{
-			mkdir(APPROOT.'data');
-		}
-		if (!is_dir(APPROOT.'data/setup'))
-		{
-			mkdir(APPROOT.'data/setup');
-		}
-		$sUID = hash('sha256', rand());
-		file_put_contents(APPROOT.'data/setup/authent', $sUID);
+		$sUID = SetupUtils::CreateSetupToken();
 		$this->oWizard->SetParameter('authent', $sUID);
 		return array('class' => 'WizStepInstallOrUpgrade', 'state' => '');
 	}
 
 	public function Display(WebPage $oPage)
@@ -2641,10 +2632,11 @@ class WizStepDone extends WizardStep
 		$sImgUrl .= '&m='.urlencode(implode(' ', $aReportedModules));
 
 		$oPage->add('<img style="border:0" src="'.$sImgUrl.'"/>');
 		$sForm = addslashes($sForm);
 		$oPage->add_ready_script("$('#wiz_form').after('$sForm');");
+		SetupUtils::EraseSetupToken();
 	}
 
 	public function CanMoveForward()
 	{
 		return false;

Vulnerable code: 	$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
	if (!file_exists(APPROOT.'data/setup/authent') || $sAuthent !== file_get_contents(APPROOT.'data/setup/authent'))
	{
		throw new SecurityException('Setup operations are not allowed outside of the setup');
		SetupPage::log_error("Setup operations are not allowed outside of the setup");
	}
	echo '<p>'.$e."</p>\n";
		SetupPage::log_info("loading file '$sFileName', peak memory usage. ".memory_get_peak_usage());
if (!function_exists('json_encode'))
{
if (!function_exists('json_decode'))
{
	function json_decode($json, $assoc=null)
$oWizard->Run();
	protected function Start()
		if ($oStep->ValidateParams($sCurrentState))
					return;
		if (!is_dir(APPROOT.'data'))
		{
			mkdir(APPROOT.'data');
		}
		if (!is_dir(APPROOT.'data/setup'))
		{
			mkdir(APPROOT.'data/setup');
		}
		$sUID = hash('sha256', rand());
		file_put_contents(APPROOT.'data/setup/authent', $sUID);
Fixed code: 	SetupUtils::CheckSetupToken();
		SetupPage::log_info("loading file peak memory usage. ".memory_get_peak_usage());
session_start();
if (!function_exists('json_encode')) {
if (!function_exists('json_decode')) {
	function json_decode($json, $assoc = null)
//N°3952
if (SetupUtils::IsSessionSetupTokenValid()) {
	// Normal operation
	$oWizard->Run();
} else {
	// Force initializing the setup
	$oWizard->Start();
	SetupUtils::CreateSetupToken();
}

	/**
	 * Create and store Setup authentication token
	 *
	 * @return string token
	 */
	public final static function CreateSetupToken()
	{
		if (!is_dir(APPROOT.'data'))
		{
			mkdir(APPROOT.'data');
		}
		if (!is_dir(APPROOT.'data/setup'))
		{
			mkdir(APPROOT.'data/setup');
		}
		$sUID = hash('sha256', rand());
		file_put_contents(APPROOT.'data/setup/authent', $sUID);
		$_SESSION['setup_token'] = $sUID;
		return $sUID;
	}

	/**
	 * Verify Setup authentication token (from the request parameter 'authent')
	 *
	 * @param bool $bRemoveToken
	 *
	 * @throws \SecurityException
	 */
	public final static function CheckSetupToken($bRemoveToken = false)
	{
		$sAuthent = utils::ReadParam('authent', '', false, 'raw_data');
		$sTokenFile = APPROOT.'data/setup/authent';
		if (!file_exists($sTokenFile) || $sAuthent !== file_get_contents($sTokenFile))
		{
			throw new SecurityException('Setup operations are not allowed outside of the setup');
		}
		if ($bRemoveToken)
		{
			@unlink($sTokenFile);
		}
	}

	/**
	 * Check setup transaction and create a new one if necessary
	 *
	 * @return bool
	 */
	public static function IsSessionSetupTokenValid()
	{
		if (isset($_SESSION['setup_token'])) {
			$sAuth = $_SESSION['setup_token'];
			$sTokenFile = APPROOT.'data/setup/authent';
			if (file_exists($sTokenFile) && $sAuth === file_get_contents($sTokenFile)) {
				return true;
			}
		}

		return false;
	}

	public static function EraseSetupToken()
	{
		$sTokenFile = APPROOT.'data/setup/authent';
		if (is_file($sTokenFile)) {
			unlink($sTokenFile);
		}
		unset($_SESSION['setup_token']);
	}
	public function Start()
		if ($oStep->ValidateParams())
					SetupUtils::EraseSetupToken();
					// Prevent token creation
					exit;
		$sUID = SetupUtils::CreateSetupToken();
		SetupUtils::EraseSetupToken();