CVE-2021-21255: 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 version 9.5.3, it was possible to switch entities with IDOR from a logged in user. This is fixed in version 9.5.4.
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: 5.7
Published: 2021-03-02T20: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]],