CVE-2020-26212 (published 2020-11-25T17:15Z)

GLPI stands for Gestionnaire Libre de Parc Informatique and it is a Free Asset and IT Management Software package, that provides ITIL Service Desk features, licenses tracking and software auditing. In GLPI before version 9.5.3, any authenticated user has read-only permissions to the planning of every other user, even admin ones. Steps to reproduce the behavior: 1. Create a new planning with 'eduardo.mozart' user (from 'IT' group that belongs to 'Super-admin') into it's personal planning at 'Assistance' > 'Planning'. 2. Copy the CalDAV url and use a CalDAV client (e.g. Thunderbird) to sync the planning with the provided URL. 3. Inform the username and password from any valid user (e.g. 'camila' from 'Proativa' group). 4. 'Camila' has read-only access to 'eduardo.mozart' personal planning. The same behavior happens to any group. E.g. 'Camila' has access to 'IT' group planning, even if she doesn't belong to this group and has a 'Self-service' profile permission). This issue is fixed in version 9.5.3. As a workaround, one can remove the `caldav.php` file to block access to CalDAV server.

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

Weakness information:
cwe_id: CWE-862
cwe_name: Missing Authorization
description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
extended_description: 
            Assuming a user with a given identity, authorization is the process of determining whether that user can access a given resource, based on the user's privileges and any permissions or other access-control specifications that apply to the resource.
            When access control checks are not applied, users are able to access data or perform actions that they should not be allowed to perform. This can lead to a wide range of problems, including information exposures, denial of service, and arbitrary code execution.
         
         

Fix information:
repo_url: https://github.com/glpi-project/glpi
commit_details: repo_url: https://github.com/glpi-project/glpi
author: Cédric Anne
author_date: 2020-11-25 09:18:56+01:00
msg: Merge pull request from GHSA-qmw3-87hr-5wgx
repository: repo_url: https://github.com/glpi-project/glpi
repo_name: glpi-project/glpi
description: GLPI is a Free Asset and IT Management Software package, Data center management, ITIL Service Desk, licenses tracking and software auditing.
repo_language: PHP
owner: glpi-project
file_changes: filename: principal.class.php
change_type: ModificationType.MODIFY
diff: @@ -37,6 +37,7 @@ if (!defined('GLPI_ROOT')) {
 }
 
 use Glpi\CalDAV\Node\Property;
+use Glpi\CalDAV\Traits\CalDAVPrincipalsTrait;
 use Glpi\CalDAV\Traits\CalDAVUriUtilTrait;
 use Sabre\DAV\PropPatch;
 use Sabre\DAVACL\PrincipalBackend\AbstractBackend;
@@ -50,6 +51,7 @@ use Sabre\DAVACL\PrincipalBackend\AbstractBackend;
  */
 class Principal extends AbstractBackend {
 
+   use CalDAVPrincipalsTrait;
    use CalDAVUriUtilTrait;
 
    const PRINCIPALS_ROOT = 'principals';
@@ -58,56 +60,17 @@ class Principal extends AbstractBackend {
 
    public function getPrincipalsByPrefix($prefixPath) {
 
-      global $DB;
-
       $principals = [];
 
       switch ($prefixPath) {
          case self::PREFIX_GROUPS:
-            if (!\Session::haveRight(\Planning::$rightname, \Planning::READALL)
-                && empty($_SESSION['glpigroups'])) {
-               // User cannot read planning of everyone and has no groups.
-               break;
-            }
-
-            $groups_criteria = getEntitiesRestrictCriteria(
-               \Group::getTable(),
-               'entities_id',
-               $_SESSION['glpiactiveentities'],
-               true
-            );
-
-            // Limit to groups visible in planning (see Planning::showAddGroupForm())
-            $groups_criteria['is_task'] = 1;
-
-            // Limit to users groups if user cannot read planning of everyone
-            if (!\Session::haveRight(\Planning::$rightname, \Planning::READALL)) {
-               $groups_criteria['id'] = $_SESSION['glpigroups'];
-            }
-
-            $groups_iterator = $DB->request(
-               [
-                  'FROM'  => \Group::getTable(),
-                  'WHERE' => $groups_criteria,
-               ]
-            );
+            $groups_iterator = $this->getVisibleGroupsIterator();
             foreach ($groups_iterator as $group_fields) {
                $principals[] = $this->getPrincipalFromGroupFields($group_fields);
             }
             break;
          case self::PREFIX_USERS:
-            if (!\Session::haveRightsOr(\Planning::$rightname, [\Planning::READALL, \Planning::READGROUP])) {
-               // Can see only personnal planning
-               $rights = 'id';
-            } else if (\Session::haveRight(\Planning::$rightname, \Planning::READGROUP)
-                && !\Session::haveRight(\Planning::$rightname, \Planning::READALL)) {
-               // Can see only planning from users sharing same groups
-               $rights = 'groups';
-            } else {
-               // Can see planning from users having rights on planning elements
-               $rights = ['change', 'problem', 'reminder', 'task', 'projecttask'];
-            }
-            $users_iterator = \User::getSqlSearchResult(false, $rights);
+            $users_iterator = $this->getVisibleUsersIterator();
             foreach ($users_iterator as $user_fields) {
                $principals[] = $this->getPrincipalFromUserFields($user_fields);
             }

programming_language: PHP
filename: acl.class.php
change_type: ModificationType.MODIFY
diff: @@ -37,10 +37,13 @@ if (!defined('GLPI_ROOT')) {
 }
 
 use Glpi\CalDAV\Backend\Principal;
+use Glpi\CalDAV\Traits\CalDAVPrincipalsTrait;
 use Glpi\CalDAV\Traits\CalDAVUriUtilTrait;
 use Sabre\CalDAV\Calendar;
 use Sabre\CalDAV\CalendarObject;
+use Sabre\DAVACL\IACL;
 use Sabre\DAVACL\Plugin;
+use Session;
 
 /**
  * ACL plugin for CalDAV server.
@@ -49,6 +52,7 @@ use Sabre\DAVACL\Plugin;
  */
 class Acl extends Plugin {
 
+   use CalDAVPrincipalsTrait;
    use CalDAVUriUtilTrait;
 
    public $principalCollectionSet = [
@@ -65,8 +69,11 @@ class Acl extends Plugin {
 
       $acl = parent::getAcl($node);
 
-      // Authenticated user have read access to all nodes, as node list only contains elements
-      // that user can read.
+      if (!($node instanceof IACL) || ($owner_path = $node->getOwner()) === null
+          || !$this->canViewPrincipalObjects($owner_path)) {
+         return $acl;
+      }
+
       $acl[] = [
          'principal' => '{DAV:}authenticated',
          'privilege' => '{DAV:}read',

programming_language: PHP
filename: caldavprincipalstrait.class.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,173 @@
+<?php
+/**
+ * ---------------------------------------------------------------------
+ * GLPI - Gestionnaire Libre de Parc Informatique
+ * Copyright (C) 2015-2020 Teclib' and contributors.
+ *
+ * http://glpi-project.org
+ *
+ * based on GLPI - Gestionnaire Libre de Parc Informatique
+ * Copyright (C) 2003-2014 by the INDEPNET Development Team.
+ *
+ * ---------------------------------------------------------------------
+ *
+ * LICENSE
+ *
+ * This file is part of GLPI.
+ *
+ * GLPI is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * GLPI is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with GLPI. If not, see <http://www.gnu.org/licenses/>.
+ * ---------------------------------------------------------------------
+ */
+
+namespace Glpi\CalDAV\Traits;
+
+use EmptyIterator;
+use Group;
+use Iterator;
+use Planning;
+use Session;
+use User;
+
+if (!defined('GLPI_ROOT')) {
+   die("Sorry. You can't access this file directly");
+}
+
+/**
+ * Trait used for CalDAV Principals listing and visibility management.
+ *
+ * @since 9.5.3
+ */
+trait CalDAVPrincipalsTrait {
+
+   /**
+    * Check if principal objects are visible for current session.
+    *
+    * @param string $path
+    *
+    * @return bool
+    */
+   protected function canViewPrincipalObjects(string $path): bool {
+      $principal_type = $this->getPrincipalItemtypeFromUri($path);
+      switch ($principal_type) {
+         case Group::class:
+            $can_view = $this->canViewGroupObjects($this->getGroupIdFromPrincipalUri($path));
+            break;
+         case User::class:
+            $can_view = $this->canViewUserObjects($this->getUsernameFromPrincipalUri($path));
+            break;
+         default:
+            $can_view = false;
+            break;
+      }
+
+      return $can_view;
+   }
+
+   /**
+    * Check if group objects are visible for current session.
+    *
+    * @param int $group_id
+    *
+    * @return bool
+    */
+   protected function canViewGroupObjects(int $group_id): bool {
+      $groups_iterator = $this->getVisibleGroupsIterator();
+      foreach ($groups_iterator as $group) {
+         if ($group['id'] === $group_id) {
+            return true;
+         }
+      }
+
+      return false;
+   }
+
+   /**
+    * Check if user objects are visible for current session.
+    *
+    * @param string $username
+    *
+    * @return bool
+    */
+   protected function canViewUserObjects(string $username): bool {
+      $users_iterator = $this->getVisibleUsersIterator();
+      foreach ($users_iterator as $user) {
+         if ($user['name'] === $username) {
+            return true;
+         }
+      }
+
+      return false;
+   }
+
+   /**
+    * Get visible groups for current session.
+    *
+    * @return array
+    */
+   protected function getVisibleGroupsIterator(): Iterator {
+
+      global $DB;
+
+      if (!Session::haveRight(Planning::$rightname, Planning::READALL)
+          && empty($_SESSION['glpigroups'])) {
+         // User cannot read planning of everyone and has no groups.
+         return new EmptyIterator();
+      }
+
+      $groups_criteria = getEntitiesRestrictCriteria(
+         Group::getTable(),
+         'entities_id',
+         $_SESSION['glpiactiveentities'],
+         true
+      );
+
+      // Limit to groups visible in planning (see Planning::showAddGroupForm())
+      $groups_criteria['is_task'] = 1;
+
+      // Limit to users groups if user cannot read planning of everyone
+      if (!Session::haveRight(Planning::$rightname, Planning::READALL)) {
+         $groups_criteria['id'] = $_SESSION['glpigroups'];
+      }
+
+      $groups_iterator = $DB->request(
+         [
+            'FROM'  => Group::getTable(),
+            'WHERE' => $groups_criteria,
+         ]
+      );
+
+      return $groups_iterator;
+   }
+
+   /**
+    * Get visible users for current session.
+    *
+    * @return array
+    */
+   protected function getVisibleUsersIterator(): Iterator {
+
+      if (!Session::haveRightsOr(Planning::$rightname, [Planning::READALL, Planning::READGROUP])) {
+         // Can see only personnal planning
+         $rights = 'id';
+      } else if (Session::haveRight(Planning::$rightname, Planning::READGROUP)
+          && !Session::haveRight(Planning::$rightname, Planning::READALL)) {
+         // Can see only planning from users sharing same groups
+         $rights = 'groups';
+      } else {
+         // Can see planning from users having rights on planning elements
+         $rights = ['change', 'problem', 'reminder', 'task', 'projecttask'];
+      }
+      return User::getSqlSearchResult(false, $rights);
+   }
+}

programming_language: PHP
filename: Server.php
change_type: ModificationType.MODIFY
diff: @@ -407,15 +407,14 @@ class Server extends DbTestCase {
     */
    public function testPropfindOnPrincipalCalendar() {
 
-      $login = TU_USER;
-      $pass  = TU_PASS;
+      $login = 'tech';
+      $pass  = 'tech';
       $user  = getItemByTypeName('User', $login);
 
-      $this->login($login, $pass);
-
       $group = new \Group();
       $group_id = (int)$group->add([
-         'name' => 'Test group'
+         'name'    => 'Test group',
+         'is_task' => 1,
       ]);
       $this->integer($group_id)->isGreaterThan(0);
       $group->getFromDB($group_id);
@@ -428,6 +427,8 @@ class Server extends DbTestCase {
          ])
       )->isGreaterThan(0);
 
+      $this->login($login, $pass);
+
       $calendars = [
          [
             'path' => 'calendars/users/' . $user->fields['name'] . '/calendar/',
@@ -488,6 +489,62 @@ class Server extends DbTestCase {
       }
    }
 
+   /**
+    * Test ACL on main objects.
+    */
+   public function testAcl() {
+
+      $user  = getItemByTypeName('User', 'tech');
+
+      $group = new \Group();
+      $group_id = (int)$group->add([
+         'name'    => 'Test group',
+         'is_task' => 1,
+      ]);
+      $this->integer($group_id)->isGreaterThan(0);
+      $group->getFromDB($group_id);
+
+      $group_user = new \Group_User();
+      $this->integer(
+         (int)$group_user->add([
+            'groups_id' => $group_id,
+            'users_id'  => $user->fields['id'],
+         ])
+      )->isGreaterThan(0);
+
+      $objects = [
+         'principals/users/' . $user->fields['name'],
+         'calendars/users/' . $user->fields['name'] . '/calendar/',
+         'principals/groups/' . $group_id,
+         'calendars/groups/' . $group_id . '/calendar/',
+      ];
+
+      $users_access = [
+         'normal' => 'HTTP/1.1 403 Forbidden',
+         'tech'   => 'HTTP/1.1 200 OK',
+      ];
+
+      foreach ($users_access as $username => $expected_status) {
+         $this->login($username, $username);
+
+         foreach ($objects as $path) {
+            $server = $this->getServerInstance('PROPFIND', $path);
+
+            $this->validateThatAuthenticationIsRequired($server);
+
+            $server->httpRequest->addHeader('Authorization', 'Basic ' . base64_encode($username . ':' . $username));
+
+            $response = new \Sabre\HTTP\Response();
+            $server->invokeMethod($server->httpRequest, $response, false);
+            $this->validateResponseIsOk($response, 207, 'application/xml'); // 207 'Multi-Status'
+
+            $xpath = $this->getXpathFromResponse($response);
+            $result_path = '/d:multistatus/d:response[1]';
+            $this->string($xpath->evaluate('string(' . $result_path . '/d:propstat/d:status)'))->isEqualTo($expected_status);
+         }
+      }
+   }
+
    /**
     * Test PROPFIND, GET, DELETE, PUT methods on calendar events.
     * Tests validates that mandatory properties are correctly set.
@@ -1529,6 +1586,7 @@ VCALENDAR
     *
     * @param \Sabre\HTTP\Response $response
     * @param integer              $status
+    * @param string|null          $content_type
     */
    private function validateResponseIsOk(\Sabre\HTTP\Response $response, int $status, string $content_type) {
       $this->integer($response->getStatus())->isEqualTo($status);

programming_language: PHP