CVE-2021-21324: Authorization Bypass Through User-Controlled Key (CWE-639)
value: GLPI is an open-source asset and IT management software package that provides ITIL Service Desk features, licenses tracking and software auditing. In GLPI before version 9.5.4 there is an Insecure Direct Object Reference (IDOR) on "Solutions". This vulnerability gives an unauthorized user the ability to enumerate GLPI items names (including users logins) using the knowbase search form (requires authentication). To Reproduce: Perform a valid authentication at your GLPI instance, Browse the ticket list and select any open ticket, click on Solution form, then Search a solution form that will redirect you to the endpoint /"glpi/front/knowbaseitem.php?item_itemtype=Ticket&item_items_id=18&forcetab=Knowbase$1", and the item_itemtype=Ticket parameter present in the previous URL will point to the PHP alias of glpi_tickets table, so just replace it with "Users" to point to glpi_users table instead; in the same way, item_items_id=18 will point to the related column id, so changing it too you should be able to enumerate all the content which has an alias. Since such id(s) are obviously incremental, a malicious party could exploit the vulnerability simply by guessing-based attempts.
CWE description: The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.
CVSS v3 base score: 6.5
Published: 2021-03-08T17:15Z
Repository: https://github.com/glpi-project/glpi
Language: PHP
Files changed: ajax/dropdownTrackingDeviceType.php, inc/computer_item.class.php, inc/dropdown.class.php, inc/user.class.php, tests/functionnal/Dropdown.php, tests/functionnal/Session.php
Commit date: 2021-03-02 09:06:34 +0100
Commit message: validate entity_restrict when available with idor tokens

Diff: diff --git a/ajax/dropdownTrackingDeviceType.php b/ajax/dropdownTrackingDeviceType.php
index 530bd1da70f..a033a2abfcb 100644
--- a/ajax/dropdownTrackingDeviceType.php
+++ b/ajax/dropdownTrackingDeviceType.php
@@ -74,11 +74,13 @@ if ($isValidItemtype) {
       'entity_restrict'     => $_POST['entity_restrict'],
       'table'               => $table,
       'multiple'            => $_POST["multiple"],
       'myname'              => $_POST["myname"],
       'rand'                => $_POST["rand"],
-      '_idor_token'         => Session::getNewIDORToken($itemtype),
+      '_idor_token'         => Session::getNewIDORToken($itemtype, [
+         'entity_restrict' => $_POST['entity_restrict'],
+      ]),
    ];
 
    if (isset($_POST["used"]) && !empty($_POST["used"])) {
       if (isset($_POST["used"][$itemtype])) {
          $p["used"] = $_POST["used"][$itemtype];
diff --git a/inc/computer_item.class.php b/inc/computer_item.class.php
index a1b8827e763..39996f4f764 100644
--- a/inc/computer_item.class.php
+++ b/inc/computer_item.class.php
@@ -680,11 +680,13 @@ class Computer_Item extends CommonDBRelation{
          'entity_restrict' => $entity_restrict,
          'fromtype'        => $fromtype,
          'itemtype'        => $itemtype,
          'onlyglobal'      => $onlyglobal,
          'used'            => $used,
-         '_idor_token'     => Session::getNewIDORToken($itemtype),
+         '_idor_token'     => Session::getNewIDORToken($itemtype, [
+            'entity_restrict' => $entity_restrict,
+         ]),
       ];
 
       echo Html::jsAjaxDropdown($myname, $field_id,
                                 $CFG_GLPI['root_doc']."/ajax/getDropdownConnect.php",
                                 $param);
diff --git a/inc/dropdown.class.php b/inc/dropdown.class.php
index 15a54e9fa11..fc254b8b484 100644
--- a/inc/dropdown.class.php
+++ b/inc/dropdown.class.php
@@ -171,15 +171,17 @@ class Dropdown {
             'displaywith'          => $params['displaywith'],
             'emptylabel'           => $params['emptylabel'],
             'condition'            => $params['condition'],
             'used'                 => $params['used'],
             'toadd'                => $params['toadd'],
-            'entity_restrict'      => (is_array($params['entity']) ? json_encode(array_values($params['entity'])) : $params['entity']),
+            'entity_restrict'      => ($entity_restrict = (is_array($params['entity']) ? json_encode(array_values($params['entity'])) : $params['entity'])),
             'on_change'            => $params['on_change'],
             'permit_select_parent' => $params['permit_select_parent'],
             'specific_tags'        => $params['specific_tags'],
-            '_idor_token'          => Session::getNewIDORToken($itemtype),
+            '_idor_token'          => Session::getNewIDORToken($itemtype, [
+               'entity_restrict' => $entity_restrict,
+            ]),
       ];
 
       $output = "<span class='no-wrap'>";
       $output.= Html::jsAjaxDropdown($params['name'], $field_id,
                                      $params['url'],
@@ -2180,10 +2182,15 @@ class Dropdown {
     * @return string|array
     */
    public static function getDropdownValue($post, $json = true) {
       global $DB, $CFG_GLPI;
 
+      // check if asked itemtype is the one originaly requested by the form
+      if (!Session::validateIDOR($post)) {
+         return;
+      }
+
       if (isset($post["entity_restrict"])
          && !is_array($post["entity_restrict"])
          && (substr($post["entity_restrict"], 0, 1) === '[')
          && (substr($post["entity_restrict"], -1) === ']')) {
          $decoded = Toolbox::jsonDecode($post['entity_restrict']);
@@ -2197,15 +2204,10 @@ class Dropdown {
       }
       if (isset($post['entity_restrict']) && 'default' === $post['entity_restrict']) {
          $post['entity_restrict'] = $_SESSION['glpiactiveentities'];
       }
 
-      // check if asked itemtype is the one originaly requested by the form
-      if (!Session::validateIDOR($post)) {
-         return;
-      }
-
       // Security
       if (!($item = getItemForItemtype($post['itemtype']))) {
          return;
       }
 
diff --git a/inc/user.class.php b/inc/user.class.php
index 0d6ea65b303..260f628ac6d 100644
--- a/inc/user.class.php
+++ b/inc/user.class.php
@@ -4024,13 +4024,16 @@ JAVASCRIPT;
          'placeholder'         => $p['placeholder'],
          'right'               => $p['right'],
          'on_change'           => $p['on_change'],
          'used'                => $p['used'],
          'inactive_deleted'    => $p['inactive_deleted'],
-         'entity_restrict'     => (is_array($p['entity']) ? json_encode(array_values($p['entity'])) : $p['entity']),
+         'entity_restrict'     => ($entity_restrict = (is_array($p['entity']) ? json_encode(array_values($p['entity'])) : $p['entity'])),
          'specific_tags'       => $p['specific_tags'],
-         '_idor_token'         => Session::getNewIDORToken(__CLASS__, ['right' => $p['right']]),
+         '_idor_token'         => Session::getNewIDORToken(__CLASS__, [
+            'right'           => $p['right'],
+            'entity_restrict' => $entity_restrict,
+         ]),
       ];
 
       $output   = Html::jsAjaxDropdown($p['name'], $field_id,
                                        $p['url'],
                                        $param);
diff --git a/tests/functionnal/Dropdown.php b/tests/functionnal/Dropdown.php
index fd110e06213..d2af60d9afa 100644
--- a/tests/functionnal/Dropdown.php
+++ b/tests/functionnal/Dropdown.php
@@ -763,11 +763,11 @@ class Dropdown extends DbTestCase {
             }
             $_SESSION[$param] = $value;
          }
       }
 
-      $params['_idor_token'] = \Session::getNewIDORToken($params['itemtype'] ?? '');
+      $params['_idor_token'] = $this->generateIdor($params);
 
       $result = \Dropdown::getDropdownValue($params, false);
 
       //reset session params before executing test
       if (count($session_params)) {
@@ -926,11 +926,11 @@ class Dropdown extends DbTestCase {
             }
             $_SESSION[$param] = $value;
          }
       }
 
-      $params['_idor_token'] = \Session::getNewIDORToken($params['itemtype'] ?? '');
+      $params['_idor_token'] = $this->generateIdor($params);
 
       $result = \Dropdown::getDropdownConnect($params, false);
 
       //reset session params before executing test
       if (count($session_params)) {
@@ -1329,6 +1329,14 @@ class Dropdown extends DbTestCase {
          ->integer['count']->isEqualTo(10)
          ->array['results']
             ->hasSize(2);
 
    }
+
+   private function generateIdor(array $params = []) {
+      $idor_add_params = [];
+      if (isset($params['entity_restrict'])) {
+         $idor_add_params['entity_restrict'] = $params['entity_restrict'];
+      }
+      return \Session::getNewIDORToken(($params['itemtype'] ?? ''), $idor_add_params);
+   }
 }
diff --git a/tests/functionnal/Session.php b/tests/functionnal/Session.php
index 0d23877cd2e..b6588a43e1c 100644
--- a/tests/functionnal/Session.php
+++ b/tests/functionnal/Session.php
@@ -373,10 +373,11 @@ class Session extends \DbTestCase {
       return [
          ['itemtype' => 'Computer'],
          ['itemtype' => 'Ticket'],
          ['itemtype' => 'Glpi\\Dashboard\\Item'],
          ['itemtype' => 'User', 'add_params' => ['right' => 'all']],
+         ['itemtype' => 'User', 'add_params' => ['entity_restrict' => 0]],
       ];
    }
 
    /**
     * @dataProvider idorProvider

Vulnerable code:       '_idor_token'         => Session::getNewIDORToken($itemtype),
         '_idor_token'     => Session::getNewIDORToken($itemtype),
            'entity_restrict'      => (is_array($params['entity']) ? json_encode(array_values($params['entity'])) : $params['entity']),
            '_idor_token'          => Session::getNewIDORToken($itemtype),
      // check if asked itemtype is the one originaly requested by the form
      if (!Session::validateIDOR($post)) {
         return;
      }

         'entity_restrict'     => (is_array($p['entity']) ? json_encode(array_values($p['entity'])) : $p['entity']),
         '_idor_token'         => Session::getNewIDORToken(__CLASS__, ['right' => $p['right']]),
      $params['_idor_token'] = \Session::getNewIDORToken($params['itemtype'] ?? '');
      $params['_idor_token'] = \Session::getNewIDORToken($params['itemtype'] ?? '');
Fixed code:       '_idor_token'         => Session::getNewIDORToken($itemtype, [
         'entity_restrict' => $_POST['entity_restrict'],
      ]),
         '_idor_token'     => Session::getNewIDORToken($itemtype, [
            'entity_restrict' => $entity_restrict,
         ]),
            'entity_restrict'      => ($entity_restrict = (is_array($params['entity']) ? json_encode(array_values($params['entity'])) : $params['entity'])),
            '_idor_token'          => Session::getNewIDORToken($itemtype, [
               'entity_restrict' => $entity_restrict,
            ]),
      // check if asked itemtype is the one originaly requested by the form
      if (!Session::validateIDOR($post)) {
         return;
      }

         'entity_restrict'     => ($entity_restrict = (is_array($p['entity']) ? json_encode(array_values($p['entity'])) : $p['entity'])),
         '_idor_token'         => Session::getNewIDORToken(__CLASS__, [
            'right'           => $p['right'],
            'entity_restrict' => $entity_restrict,
         ]),
      $params['_idor_token'] = $this->generateIdor($params);
      $params['_idor_token'] = $this->generateIdor($params);

   private function generateIdor(array $params = []) {
      $idor_add_params = [];
      if (isset($params['entity_restrict'])) {
         $idor_add_params['entity_restrict'] = $params['entity_restrict'];
      }
      return \Session::getNewIDORToken(($params['itemtype'] ?? ''), $idor_add_params);
   }
         ['itemtype' => 'User', 'add_params' => ['entity_restrict' => 0]],