CVE-2022-24780 (published 2022-04-05T19:15Z)

Combodo iTop is a web based IT Service Management tool. In versions prior to 2.7.6 and 3.0.0, users of the iTop user portal can send TWIG code to the server by forging specific http queries, and execute arbitrary code on the server using http server user privileges. This issue is fixed in versions 2.7.6 and 3.0.0. There are currently no known workarounds.

Severity: MEDIUM
CVSS v2 base score: 6.5 (AV:N/AC:L/Au:S/C:P/I:P/A:P)
CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-94
cwe_name: Improper Control of Generation of Code ('Code Injection')
description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
extended_description: 
            When a product allows a user's input to contain code syntax, it might be possible for an attacker to craft the code in such a way that it will alter the intended control flow of the product. Such an alteration could lead to arbitrary code execution.
            Injection problems encompass a wide variety of issues -- all mitigated in very different ways. For this reason, the most effective way to discuss these weaknesses is to note the distinct features which classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed. The most classic instantiations of this category of weakness are SQL injection and format string vulnerabilities.
         
         

Fix information:
repo_url: https://github.com/Combodo/iTop
commit_details: repo_url: https://github.com/Combodo/iTop
author: Pierre Goiffon
author_date: 2021-11-25 12:59:19+01:00
msg: N°4384 Security hardening
repository: repo_url: https://github.com/Combodo/iTop
repo_name: Combodo/iTop
description: A simple, web based IT Service Management tool 
repo_language: PHP
owner: Combodo
file_changes: filename: ObjectFormManager.php
change_type: ModificationType.MODIFY
diff: @@ -91,26 +91,34 @@ class ObjectFormManager extends FormManager
 	 * - formmode : view|edit|create
 	 * - values for parent
 	 *
+	 * @param bool $bTrustContent if false then won't allow TWIG content
+	 *
 	 * @inheritDoc
 	 * @throws \Exception
+	 * @throws \SecurityException if twig content is present and $bTrustContent is false
 	 */
-	static function FromJSON($sJson)
+	public static function FromJSON($sJson, $bTrustContent = false)
 	{
-		if (is_array($sJson))
-		{
+		if (is_array($sJson)) {
 			$aJson = $sJson;
-		}
-		else
-		{
+		} else {
 			$aJson = json_decode($sJson, true);
 		}
 
+		if (false === $bTrustContent) {
+			/** @noinspection NestedPositiveIfStatementsInspection */
+			if (isset($aJson['formproperties']['layout']['type']) && ($aJson['formproperties']['layout']['type'] === 'twig')) {
+				// There will be an IssueLog above in the hierarchy due to the exception, but we are logging here so that we can output the JSON data !
+				IssueLog::Error('Portal received a query with forbidden twig content!', \LogChannels::PORTAL, ['formmanager_data' => $aJson]);
+				throw new \SecurityException('Twig content not allowed in this context!');
+			}
+		}
+
 		/** @var \Combodo\iTop\Portal\Form\ObjectFormManager $oFormManager */
 		$oFormManager = parent::FromJSON($sJson);
 
 		// Retrieving object to edit
-		if (!isset($aJson['formobject_class']))
-		{
+		if (!isset($aJson['formobject_class'])) {
 			throw new Exception('Object class must be defined in order to generate the form');
 		}
 		$sObjectClass = $aJson['formobject_class'];
@@ -151,14 +159,44 @@ class ObjectFormManager extends FormManager
 		}
 
 		// Retrieving callback urls
-		if (!isset($aJson['formcallbacks']))
-		{
+		if (!isset($aJson['formcallbacks'])) {
 			// TODO
 		}
 
 		return $oFormManager;
 	}
 
+	/**
+	 * @param string $sPostedFormManagerData received data from the browser
+	 * @param array $aOriginalFormProperties data generated server side
+	 *
+	 * @return bool true if the data are identical
+	 *
+	 * @since 2.7.6 3.0.0 N°4384 check formmanager_data
+	 */
+	public static function CanTrustFormLayoutContent($sPostedFormManagerData, $aOriginalFormProperties)
+	{
+		$aPostedFormManagerData = json_decode($sPostedFormManagerData, true);
+		$sPostedFormLayoutType = (isset($aPostedFormManagerData['formproperties']['layout']['type'])) ? $aPostedFormManagerData['formproperties']['layout']['type'] : '';
+
+		if ($sPostedFormLayoutType === 'xhtml') {
+			return true;
+		}
+
+		// we need to parse the content so that autoclose tags are returned correctly (`<div />` => `<div></div>`)
+		$oHtmlDocument = new \DOMDocument();
+
+		$sPostedFormLayoutContent = (isset($aPostedFormManagerData['formproperties']['layout']['content'])) ? $aPostedFormManagerData['formproperties']['layout']['content'] : '';
+		$oHtmlDocument->loadXML('<root>'.$sPostedFormLayoutContent.'</root>');
+		$sPostedFormLayoutRendered = $oHtmlDocument->saveHTML();
+
+		$sOriginalFormLayoutContent = (isset($aOriginalFormProperties['layout']['content'])) ? $aOriginalFormProperties['layout']['content'] : '';
+		$oHtmlDocument->loadXML('<root>'.$sOriginalFormLayoutContent.'</root>');
+		$sOriginalFormLayoutContentRendered = $oHtmlDocument->saveHTML();
+
+		return ($sPostedFormLayoutRendered === $sOriginalFormLayoutContentRendered);
+	}
+
 	/**
 	 *
 	 * @return \Symfony\Component\DependencyInjection\ContainerInterface

programming_language: PHP
filename: ObjectFormHandlerHelper.php
change_type: ModificationType.MODIFY
diff: @@ -108,10 +108,10 @@ class ObjectFormHandlerHelper
 
 	/**
 	 * @param \Symfony\Component\HttpFoundation\Request $oRequest
-	 * @param string                                    $sMode
-	 * @param string                                    $sObjectClass
-	 * @param string                                    $sObjectId
-	 * @param string                                    $aFormProperties
+	 * @param string $sMode
+	 * @param string $sObjectClass
+	 * @param string $sObjectId
+	 * @param array $aFormProperties
 	 *
 	 * @return array
 	 *
@@ -127,9 +127,10 @@ class ObjectFormHandlerHelper
 		$bModal = ($oRequest->isXmlHttpRequest() && empty($sOperation));
 
 		// - Retrieve form properties
+		$aOriginalFormProperties = ApplicationHelper::GetLoadedFormFromClass($this->aCombodoPortalInstanceConf['forms'], $sObjectClass, $sMode);
 		if ($aFormProperties === null)
 		{
-			$aFormProperties = ApplicationHelper::GetLoadedFormFromClass($this->aCombodoPortalInstanceConf['forms'], $sObjectClass, $sMode);
+			$aFormProperties = $aOriginalFormProperties;
 		}
 
 		// - Create and
@@ -281,27 +282,24 @@ class ObjectFormHandlerHelper
 
 			$oFormManager->Build();
 			$aFormData['hidden_fields'] = $oFormManager->GetHiddenFieldsId();
-				// Check the number of editable fields
+			// Check the number of editable fields
 			$aFormData['editable_fields_count'] = $oFormManager->GetForm()->GetEditableFieldCount();
-		}
-		else
-		{
+		} else {
 			// Update / Submit / Cancel
+			/** @var \Combodo\iTop\Portal\Form\ObjectFormManager $sFormManagerClass */
 			$sFormManagerClass = $this->oRequestManipulator->ReadParam('formmanager_class', '', FILTER_UNSAFE_RAW);
 			$sFormManagerData = $this->oRequestManipulator->ReadParam('formmanager_data', '', FILTER_UNSAFE_RAW);
-			if (empty($sFormManagerClass) || empty($sFormManagerData))
-			{
+			if (empty($sFormManagerClass) || empty($sFormManagerData)) {
 				IssueLog::Error(__METHOD__.' at line '.__LINE__.' : Parameters formmanager_class and formamanager_data must be defined.');
 				throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, 'Parameters formmanager_class and formmanager_data must be defined.');
 			}
 
-			/** @var \Combodo\iTop\Portal\Form\ObjectFormManager $oFormManager */
-			$oFormManager = $sFormManagerClass::FromJSON($sFormManagerData);
+			$bTrustContent = $sFormManagerClass::CanTrustFormLayoutContent($sFormManagerData, $aOriginalFormProperties);
+			$oFormManager = $sFormManagerClass::FromJSON($sFormManagerData, $bTrustContent);
 			$oFormManager->SetContainer($this->oContainer);
 
 			// Applying action rules if present
-			if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== ''))
-			{
+			if (($oFormManager->GetActionRulesToken() !== null) && ($oFormManager->GetActionRulesToken() !== '')) {
 				$aActionRules = ContextManipulatorHelper::DecodeRulesToken($oFormManager->GetActionRulesToken());
 				$oObj = $oFormManager->GetObject();
 				$this->oContextManipulator->PrepareObject($aActionRules, $oObj);

programming_language: PHP
filename: formmanager.class.inc.php
change_type: ModificationType.MODIFY
diff: @@ -40,7 +40,8 @@ abstract class FormManager
 	 * - formrenderer_class : The class of the FormRenderer to use in the FormManager
 	 * - formrenderer_endpoint : The endpoint of the renderer
 	 *
-	 * @param string $sJson
+	 * @param string|string[] $sJson
+	 *
 	 * @return $this
 	 */
 	static function FromJSON($sJson)

programming_language: PHP
repo_url: https://github.com/Combodo/iTop
commit_details: repo_url: https://github.com/Combodo/iTop
author: Pierre Goiffon
author_date: 2021-12-10 17:10:34+01:00
msg: N°4384 Fix PHP warning when decoding formmanager_data when it is already in a PHP array form
repository: repo_url: https://github.com/Combodo/iTop
repo_name: Combodo/iTop
description: A simple, web based IT Service Management tool 
repo_language: PHP
owner: Combodo
file_changes: filename: ObjectFormManager.php
change_type: ModificationType.MODIFY
diff: @@ -79,11 +79,28 @@ class ObjectFormManager extends FormManager
 	protected $aCallbackUrls = array();
 	/**
 	 * List of hidden fields, used for form update (eg. remove them from the form regarding they dependencies)
+	 *
 	 * @var array $aHiddenFieldsId
 	 * @since 2.7.5
 	 */
 	protected $aHiddenFieldsId = array();
 
+	/**
+	 * @param string|array $formManagerData value of the formmanager_data portal parameter, either JSON or object
+	 *
+	 * @return array formmanager_data as a PHP array
+	 *
+	 * @since 2.7.6 3.0.0 N°4384 method creation : factorize as this is used twice now
+	 */
+	protected static function DecodeFormManagerData($formManagerData)
+	{
+		if (is_array($formManagerData)) {
+			return $formManagerData;
+		}
+
+		return json_decode($formManagerData, true);
+	}
+
 	/**
 	 * @param string $sJson JSON data that must contain at least :
 	 *       - formobject_class : The class of the object that is being edited/viewed
@@ -101,11 +118,7 @@ class ObjectFormManager extends FormManager
 	 */
 	public static function FromJSON($sJson, $bTrustContent = false)
 	{
-		if (is_array($sJson)) {
-			$aJson = $sJson;
-		} else {
-			$aJson = json_decode($sJson, true);
-		}
+		$aJson = static::DecodeFormManagerData($sJson);
 
 		$oConfig = utils::GetConfig();
 		$bIsContentCheckEnabled = $oConfig->GetModuleSetting(PORTAL_ID, 'enable_formmanager_content_check', true);
@@ -180,7 +193,7 @@ class ObjectFormManager extends FormManager
 	 */
 	public static function CanTrustFormLayoutContent($sPostedFormManagerData, $aOriginalFormProperties)
 	{
-		$aPostedFormManagerData = json_decode($sPostedFormManagerData, true);
+		$aPostedFormManagerData = static::DecodeFormManagerData($sPostedFormManagerData);
 		$sPostedFormLayoutType = (isset($aPostedFormManagerData['formproperties']['layout']['type'])) ? $aPostedFormManagerData['formproperties']['layout']['type'] : '';
 
 		if ($sPostedFormLayoutType === 'xhtml') {

programming_language: PHP
repo_url: https://github.com/Combodo/iTop
commit_details: repo_url: https://github.com/Combodo/iTop
author: Pierre Goiffon
author_date: 2021-12-09 11:57:20+01:00
msg: N°4384 Security hardening
Module parameter flag for extensions
repository: repo_url: https://github.com/Combodo/iTop
repo_name: Combodo/iTop
description: A simple, web based IT Service Management tool 
repo_language: PHP
owner: Combodo
file_changes: filename: ObjectFormManager.php
change_type: ModificationType.MODIFY
diff: @@ -85,16 +85,19 @@ class ObjectFormManager extends FormManager
 	protected $aHiddenFieldsId = array();
 
 	/**
-	 * Creates an instance of \Combodo\iTop\Portal\Form\ObjectFormManager from JSON data that must contain at least :
-	 * - formobject_class : The class of the object that is being edited/viewed
-	 * - formmode : view|edit|create
-	 * - values for parent
+	 * @param string $sJson JSON data that must contain at least :
+	 *       - formobject_class : The class of the object that is being edited/viewed
+	 *       - formmode : view|edit|create
+	 *       - values for parent
+	 * @param bool $bTrustContent if false then won't allow modified TWIG content
 	 *
-	 * @param bool $bTrustContent if false then won't allow TWIG content
+	 * @return \Combodo\iTop\Portal\Form\ObjectFormManager new instance init from JSON data
 	 *
 	 * @inheritDoc
 	 * @throws \Exception
 	 * @throws \SecurityException if twig content is present and $bTrustContent is false
+	 *
+	 * @since 2.7.6 3.0.0 N°4384 new $bTrustContent parameter
 	 */
 	public static function FromJSON($sJson, $bTrustContent = false)
 	{
@@ -104,7 +107,9 @@ class ObjectFormManager extends FormManager
 			$aJson = json_decode($sJson, true);
 		}
 
-		if (false === $bTrustContent) {
+		$oConfig = utils::GetConfig();
+		$bIsContentCheckEnabled = $oConfig->GetModuleSetting(PORTAL_ID, 'enable_formmanager_content_check', true);
+		if ($bIsContentCheckEnabled && (false === $bTrustContent)) {
 			/** @noinspection NestedPositiveIfStatementsInspection */
 			if (isset($aJson['formproperties']['layout']['type']) && ($aJson['formproperties']['layout']['type'] === 'twig')) {
 				// There will be an IssueLog above in the hierarchy due to the exception, but we are logging here so that we can output the JSON data !

programming_language: PHP
filename: datamodel.itop-tickets.xml
change_type: ModificationType.MODIFY
diff: @@ -1733,6 +1733,7 @@
   <module_parameters>
     <parameters id="itop-portal" _delta="define">
       <lazy_loading_threshold>500</lazy_loading_threshold><!-- Will be placed in the conf/<env>/config-itop.php file under the itop-portal module's settings  -->
+      <enable_formmanager_content_check type="boolean">true</enable_formmanager_content_check>
     </parameters>
   </module_parameters>
 </itop_design>

programming_language: PHP