CVE-2024-27098: Insufficient Information (NVD-CWE-noinfo)
value: GLPI is a Free Asset and IT Management Software package, Data center management, ITIL Service Desk, licenses tracking and software auditing. An authenticated user can execute a SSRF based attack using Arbitrary Object Instantiation. This issue has been patched in version 10.0.13.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-18T17:15Z
Repository: https://github.com/glpi-project/glpi
Language: PHP
Files changed: ajax/comments.php, ajax/dropdownLocation.php, ajax/kanban.php, ajax/rule.php, ajax/updateTranslationFields.php, ajax/updateTranslationValue.php, composer.json, front/device.php, front/devicemodel.form.php, front/devicemodel.php, front/devicetype.form.php, front/devicetype.php, front/item_device.php, front/itilfollowup.form.php, front/itilsolution.form.php, src/RuleAction.php, tests/web/APIRest.php, tools/patches/guzzle-http-client-restrict-http-methods.patch
Commit date: 2024-03-13 08:15:19 +0100
Commit message: Merge pull request from GHSA-92x4-q9w5-837w

* Check for valid itemtype before creating objects

* Restrict HTTP methods allowed by Guzzle client

---------

Co-authored-by: Cédric Anne <cedric.anne@gmail.com>
Diff: diff --git a/ajax/comments.php b/ajax/comments.php
index 8adba3905c82..f962c6f958ed 100644
--- a/ajax/comments.php
+++ b/ajax/comments.php
@@ -118,11 +118,11 @@ if (
                     attr('href', '" . $_POST['itemtype']::getFormURLWithID($_POST["value"]) . "');";
                     echo "</script>\n";
                 }
 
                 if (isset($_POST['with_dc_position'])) {
-                    $item = new $_POST['itemtype']();
+                    $item = getItemForItemtype($_POST['itemtype']);
                     echo "<script type='text/javascript' >\n";
 
                    //if item have a DC position (reload url to it's rack)
                     if ($rack = $item->isRackPart($_POST['itemtype'], $_POST["value"], true)) {
                         echo Html::jsGetElementbyID($_POST['with_dc_position']) . ".
diff --git a/ajax/dropdownLocation.php b/ajax/dropdownLocation.php
index 89cbce4f6298..e398cd877b6a 100644
--- a/ajax/dropdownLocation.php
+++ b/ajax/dropdownLocation.php
@@ -43,11 +43,11 @@ if (
     && !is_subclass_of($_REQUEST['itemtype'], 'CommonDBTM')
 ) {
     throw new \RuntimeException('Required argument missing or incorrect!');
 }
 
-$item = new $_REQUEST['itemtype']();
+$item = getItemForItemtype($_REQUEST['itemtype']);
 $item->getFromDB((int) $_REQUEST['items_id']);
 
 $locations_id = $item->fields['locations_id'] ?? 0;
 
 $entities_id = $item->fields['entities_id'] ?? $_SESSION['glpiactive_entity'];
diff --git a/ajax/kanban.php b/ajax/kanban.php
index e418215fdc8d..f42a5490c740 100644
--- a/ajax/kanban.php
+++ b/ajax/kanban.php
@@ -168,11 +168,11 @@ if (($_POST['action'] ?? null) === 'update') {
         }
     }
 } else if (($_POST['action'] ?? null) === 'move_item') {
     $checkParams(['card', 'column', 'position', 'kanban']);
     /** @var Kanban|CommonDBTM $kanban */
-    $kanban = new $_POST['kanban']['itemtype']();
+    $kanban = getItemForItemtype($_POST['kanban']['itemtype']);
     $can_move = $kanban->canOrderKanbanCard($_POST['kanban']['items_id']);
     if ($can_move) {
         Item_Kanban::moveCard(
             $_POST['kanban']['itemtype'],
             $_POST['kanban']['items_id'],
diff --git a/ajax/rule.php b/ajax/rule.php
index 59adbb072cd9..a079dc0be925 100644
--- a/ajax/rule.php
+++ b/ajax/rule.php
@@ -39,10 +39,10 @@ include('../inc/includes.php');
 Session::checkLoginUser();
 
 switch ($_REQUEST['action']) {
     case "move_rule":
         if (is_subclass_of($_POST['collection_classname'], RuleCollection::getType())) {
-            $rule_collection = new $_POST['collection_classname']();
+            $rule_collection = getItemForItemtype($_POST['collection_classname']);
             $rule_collection->moveRule((int) $_POST['rule_id'], (int) $_POST['ref_id'], $_POST['sort_action']);
         }
         break;
 }
diff --git a/ajax/updateTranslationFields.php b/ajax/updateTranslationFields.php
index 6c2ffe772899..a0338dd0edf7 100644
--- a/ajax/updateTranslationFields.php
+++ b/ajax/updateTranslationFields.php
@@ -42,9 +42,9 @@ include('../inc/includes.php');
 header("Content-Type: text/html; charset=UTF-8");
 Html::header_nocache();
 
 Session::checkRight("dropdown", UPDATE);
 if (isset($_POST['itemtype']) && isset($_POST['language'])) {
-    $item = new $_POST['itemtype']();
+    $item = getItemForItemtype($_POST['itemtype']);
     $item->getFromDB($_POST['items_id']);
     DropdownTranslation::dropdownFields($item, $_POST['language']);
 }
diff --git a/ajax/updateTranslationValue.php b/ajax/updateTranslationValue.php
index 7c645cc5dbfb..abae461fa4fa 100644
--- a/ajax/updateTranslationValue.php
+++ b/ajax/updateTranslationValue.php
@@ -41,11 +41,11 @@ Html::header_nocache();
 Session::checkRight("dropdown", UPDATE);
 
 $matching_field = null;
 
 if (isset($_POST['itemtype'], $_POST['field']) && is_a($_POST['itemtype'], CommonDropdown::class, true)) {
-    $itemtype = new $_POST['itemtype']();
+    $itemtype = getItemForItemtype($_POST['itemtype']);
     $matching_field = $itemtype->getAdditionalField($_POST['field']);
 }
 
 if (($matching_field['type'] ?? null) === 'tinymce') {
     Html::textarea([
diff --git a/composer.json b/composer.json
index 46116e572a06..0a1d74336236 100644
--- a/composer.json
+++ b/composer.json
@@ -143,19 +143,21 @@
             "@php -r \"file_put_contents('.composer.hash', sha1_file('composer.lock'));\"",
             "@php -f vendor/bin/build_hw_jsons",
             "patch -f -p1 -d vendor/laminas/laminas-mail/ < tools/patches/laminas-mail-invalid-header-ignore.patch || true",
             "patch -f -p1 -d vendor/laminas/laminas-mail/ < tools/patches/laminas-mail-address-no-length-check.patch || true",
             "patch -f -p1 -d vendor/atoum/atoum/ < tools/patches/atoum-4.1.0...4.2.0.patch || true",
-            "patch -f -p1 -d vendor/michelf/php-markdown/ < tools/patches/php-markdown-php8-compat.patch || true"
+            "patch -f -p1 -d vendor/michelf/php-markdown/ < tools/patches/php-markdown-php8-compat.patch || true",
+            "patch -f -p1 -d vendor/guzzlehttp/guzzle/ < tools/patches/guzzle-http-client-restrict-http-methods.patch || true"
         ],
         "post-update-cmd": [
             "@php -r \"file_put_contents('.composer.hash', sha1_file('composer.lock'));\"",
             "@php -f vendor/bin/build_hw_jsons",
             "patch -f -p1 -d vendor/laminas/laminas-mail/ < tools/patches/laminas-mail-invalid-header-ignore.patch || true",
             "patch -f -p1 -d vendor/laminas/laminas-mail/ < tools/patches/laminas-mail-address-no-length-check.patch || true",
             "patch -f -p1 -d vendor/atoum/atoum/ < tools/patches/atoum-4.1.0...4.2.0.patch || true",
-            "patch -f -p1 -d vendor/michelf/php-markdown/ < tools/patches/php-markdown-php8-compat.patch || true"
+            "patch -f -p1 -d vendor/michelf/php-markdown/ < tools/patches/php-markdown-php8-compat.patch || true",
+            "patch -f -p1 -d vendor/guzzlehttp/guzzle/ < tools/patches/guzzle-http-client-restrict-http-methods.patch || true"
         ],
         "build": [
             "bin/console dependencies install && bin/console locales:compile"
         ]
     },
diff --git a/front/device.php b/front/device.php
index 079f5a53f455..1ccab567e73a 100644
--- a/front/device.php
+++ b/front/device.php
@@ -39,7 +39,7 @@ if (!isset($_GET['itemtype']) || !class_exists($_GET['itemtype'])) {
     throw new \RuntimeException(
         'Missing or incorrect device type called!'
     );
 }
 
-$dropdown = new $_GET['itemtype']();
+$dropdown = getItemForItemtype($_GET['itemtype']);
 include(GLPI_ROOT . "/front/dropdown.common.php");
diff --git a/front/devicemodel.form.php b/front/devicemodel.form.php
index bcb6761e96d4..bb10ea87eba1 100644
--- a/front/devicemodel.form.php
+++ b/front/devicemodel.form.php
@@ -39,7 +39,7 @@ if (!isset($_GET['itemtype']) || !class_exists($_GET['itemtype'])) {
     throw new \RuntimeException(
         'Missing or incorrect device type called!'
     );
 }
 
-$dropdown = new $_GET['itemtype']();
+$dropdown = getItemForItemtype($_GET['itemtype']);
 include(GLPI_ROOT . "/front/dropdown.common.form.php");
diff --git a/front/devicemodel.php b/front/devicemodel.php
index 079f5a53f455..1ccab567e73a 100644
--- a/front/devicemodel.php
+++ b/front/devicemodel.php
@@ -39,7 +39,7 @@ if (!isset($_GET['itemtype']) || !class_exists($_GET['itemtype'])) {
     throw new \RuntimeException(
         'Missing or incorrect device type called!'
     );
 }
 
-$dropdown = new $_GET['itemtype']();
+$dropdown = getItemForItemtype($_GET['itemtype']);
 include(GLPI_ROOT . "/front/dropdown.common.php");
diff --git a/front/devicetype.form.php b/front/devicetype.form.php
index bcb6761e96d4..bb10ea87eba1 100644
--- a/front/devicetype.form.php
+++ b/front/devicetype.form.php
@@ -39,7 +39,7 @@ if (!isset($_GET['itemtype']) || !class_exists($_GET['itemtype'])) {
     throw new \RuntimeException(
         'Missing or incorrect device type called!'
     );
 }
 
-$dropdown = new $_GET['itemtype']();
+$dropdown = getItemForItemtype($_GET['itemtype']);
 include(GLPI_ROOT . "/front/dropdown.common.form.php");
diff --git a/front/devicetype.php b/front/devicetype.php
index 079f5a53f455..1ccab567e73a 100644
--- a/front/devicetype.php
+++ b/front/devicetype.php
@@ -39,7 +39,7 @@ if (!isset($_GET['itemtype']) || !class_exists($_GET['itemtype'])) {
     throw new \RuntimeException(
         'Missing or incorrect device type called!'
     );
 }
 
-$dropdown = new $_GET['itemtype']();
+$dropdown = getItemForItemtype($_GET['itemtype']);
 include(GLPI_ROOT . "/front/dropdown.common.php");
diff --git a/front/item_device.php b/front/item_device.php
index 66da8111142c..f45c29d8f6c3 100644
--- a/front/item_device.php
+++ b/front/item_device.php
@@ -42,11 +42,11 @@ if (!isset($_GET['itemtype']) || !class_exists($_GET['itemtype'])) {
     throw new \RuntimeException(
         'Missing or incorrect item device type called!'
     );
 }
 
-$itemDevice = new $_GET['itemtype']();
+$itemDevice = getItemForItemtype($_GET['itemtype']);
 if (!$itemDevice->canView()) {
     Session::redirectIfNotLoggedIn();
     Html::displayRightError();
 }
 
diff --git a/front/itilfollowup.form.php b/front/itilfollowup.form.php
index a6b52f87b1a9..781e471b2142 100644
--- a/front/itilfollowup.form.php
+++ b/front/itilfollowup.form.php
@@ -48,11 +48,11 @@ $redirect = null;
 $handled = false;
 
 if (!isset($_POST['itemtype']) || !class_exists($_POST['itemtype'])) {
     Html::displayErrorAndDie('Lost');
 }
-$track = new $_POST['itemtype']();
+$track = getItemForItemtype($_POST['itemtype']);
 
 
 if (isset($_POST["add"])) {
     $fup->check(-1, CREATE, $_POST);
     $fup->add($_POST);
diff --git a/front/itilsolution.form.php b/front/itilsolution.form.php
index 010d60e0e407..98e2c29558dc 100644
--- a/front/itilsolution.form.php
+++ b/front/itilsolution.form.php
@@ -41,11 +41,11 @@ global $DB;
 include('../inc/includes.php');
 
 Session::checkLoginUser();
 
 $solution = new ITILSolution();
-$track = new $_POST['itemtype']();
+$track = getItemForItemtype($_POST['itemtype']);
 $track->getFromDB($_POST['items_id']);
 
 $redirect = null;
 $handled = false;
 
diff --git a/src/RuleAction.php b/src/RuleAction.php
index 20a49c2a40e6..c143f6e29221 100644
--- a/src/RuleAction.php
+++ b/src/RuleAction.php
@@ -49,11 +49,11 @@ class RuleAction extends CommonDBChild
 
         $forbidden   = parent::getForbiddenStandardMassiveAction();
         $forbidden[] = 'update';
 
         if (isset($_POST['rule_class_name']) && is_subclass_of(\Rule::class, $_POST['rule_class_name'])) {
-            $rule = new $_POST['rule_class_name']();
+            $rule = getItemForItemtype($_POST['rule_class_name']);
             if ($rule->maxActionsCount() == 1) {
                 $forbidden[] = 'clone';
             }
         }
         //maxActionsCount on Rule
diff --git a/tests/web/APIRest.php b/tests/web/APIRest.php
index 089287358f77..650ae40ab7fe 100644
--- a/tests/web/APIRest.php
+++ b/tests/web/APIRest.php
@@ -103,21 +103,15 @@ class APIRest extends APIBaseClass
         }
         if (isset($params['multipart'])) {
            // Guzzle lib will automatically push the correct Content-type
             unset($params['headers']['Content-Type']);
         }
-        $verb = strtolower($verb);
-        if (in_array($verb, ['get', 'post', 'delete', 'put', 'options', 'patch'])) {
-            try {
-                return $this->http_client->{$verb}(
-                    $this->base_uri . $relative_uri,
-                    $params
-                );
-            } catch (\Throwable $e) {
-                throw $e;
-            }
-        }
+        return $this->http_client->request(
+            $verb,
+            $this->base_uri . $relative_uri,
+            $params
+        );
     }
 
     protected function query(
         $resource = "",
         $params = [],
diff --git a/tools/patches/guzzle-http-client-restrict-http-methods.patch b/tools/patches/guzzle-http-client-restrict-http-methods.patch
new file mode 100644
index 000000000000..dd992fcd6519
--- /dev/null
+++ b/tools/patches/guzzle-http-client-restrict-http-methods.patch
@@ -0,0 +1,33 @@
+diff --git a/src/Client.php b/src/Client.php
+index 58f1d891..7448bb49 100644
+--- a/src/Client.php
++++ b/src/Client.php
+@@ -70,28 +70,6 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
+         $this->configureDefaults($config);
+     }
+ 
+-    /**
+-     * @param string $method
+-     * @param array  $args
+-     *
+-     * @return PromiseInterface|ResponseInterface
+-     *
+-     * @deprecated Client::__call will be removed in guzzlehttp/guzzle:8.0.
+-     */
+-    public function __call($method, $args)
+-    {
+-        if (\count($args) < 1) {
+-            throw new InvalidArgumentException('Magic request methods require a URI and optional options array');
+-        }
+-
+-        $uri = $args[0];
+-        $opts = $args[1] ?? [];
+-
+-        return \substr($method, -5) === 'Async'
+-            ? $this->requestAsync(\substr($method, 0, -5), $uri, $opts)
+-            : $this->request($method, $uri, $opts);
+-    }
+-
+     /**
+      * Asynchronously send an HTTP request.
+      *

Vulnerable code:                     $item = new $_POST['itemtype']();
$item = new $_REQUEST['itemtype']();
    $kanban = new $_POST['kanban']['itemtype']();
            $rule_collection = new $_POST['collection_classname']();
    $item = new $_POST['itemtype']();
    $itemtype = new $_POST['itemtype']();
            "patch -f -p1 -d vendor/michelf/php-markdown/ < tools/patches/php-markdown-php8-compat.patch || true"
            "patch -f -p1 -d vendor/michelf/php-markdown/ < tools/patches/php-markdown-php8-compat.patch || true"
$dropdown = new $_GET['itemtype']();
$dropdown = new $_GET['itemtype']();
$dropdown = new $_GET['itemtype']();
$dropdown = new $_GET['itemtype']();
$dropdown = new $_GET['itemtype']();
$itemDevice = new $_GET['itemtype']();
$track = new $_POST['itemtype']();
$track = new $_POST['itemtype']();
            $rule = new $_POST['rule_class_name']();
        $verb = strtolower($verb);
        if (in_array($verb, ['get', 'post', 'delete', 'put', 'options', 'patch'])) {
            try {
                return $this->http_client->{$verb}(
                    $this->base_uri . $relative_uri,
                    $params
                );
            } catch (\Throwable $e) {
                throw $e;
            }
        }
Fixed code:                     $item = getItemForItemtype($_POST['itemtype']);
$item = getItemForItemtype($_REQUEST['itemtype']);
    $kanban = getItemForItemtype($_POST['kanban']['itemtype']);
            $rule_collection = getItemForItemtype($_POST['collection_classname']);
    $item = getItemForItemtype($_POST['itemtype']);
    $itemtype = getItemForItemtype($_POST['itemtype']);
            "patch -f -p1 -d vendor/michelf/php-markdown/ < tools/patches/php-markdown-php8-compat.patch || true",
            "patch -f -p1 -d vendor/guzzlehttp/guzzle/ < tools/patches/guzzle-http-client-restrict-http-methods.patch || true"
            "patch -f -p1 -d vendor/michelf/php-markdown/ < tools/patches/php-markdown-php8-compat.patch || true",
            "patch -f -p1 -d vendor/guzzlehttp/guzzle/ < tools/patches/guzzle-http-client-restrict-http-methods.patch || true"
$dropdown = getItemForItemtype($_GET['itemtype']);
$dropdown = getItemForItemtype($_GET['itemtype']);
$dropdown = getItemForItemtype($_GET['itemtype']);
$dropdown = getItemForItemtype($_GET['itemtype']);
$dropdown = getItemForItemtype($_GET['itemtype']);
$itemDevice = getItemForItemtype($_GET['itemtype']);
$track = getItemForItemtype($_POST['itemtype']);
$track = getItemForItemtype($_POST['itemtype']);
            $rule = getItemForItemtype($_POST['rule_class_name']);
        return $this->http_client->request(
            $verb,
            $this->base_uri . $relative_uri,
            $params
        );
diff --git a/src/Client.php b/src/Client.php
index 58f1d891..7448bb49 100644
--- a/src/Client.php
@@ -70,28 +70,6 @@ class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
         $this->configureDefaults($config);
     }
 
-    /**
-     * @param string $method
-     * @param array  $args
-     *
-     * @return PromiseInterface|ResponseInterface
-     *
-     * @deprecated Client::__call will be removed in guzzlehttp/guzzle:8.0.
-     */
-    public function __call($method, $args)
-    {
-        if (\count($args) < 1) {
-            throw new InvalidArgumentException('Magic request methods require a URI and optional options array');
-        }
-
-        $uri = $args[0];
-        $opts = $args[1] ?? [];
-
-        return \substr($method, -5) === 'Async'
-            ? $this->requestAsync(\substr($method, 0, -5), $uri, $opts)
-            : $this->request($method, $uri, $opts);
-    }
-
     /**
      * Asynchronously send an HTTP request.
      *