CVE-2020-26212 (PHP, published 2020-11-25T17:15Z)
Description: value: 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 v3 base score: 6.5)
Weakness: CWE-862 \u2014 Missing Authorization
The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
Repository: https://github.com/glpi-project/glpi
Files changed:
inc/caldav/backend/principal.class.php
inc/caldav/plugin/acl.class.php
inc/caldav/traits/caldavprincipalstrait.class.php
tests/functionnal/Glpi/CalDAV/Server.php
Commit message: Merge pull request from GHSA-qmw3-87hr-5wgx



Vulnerable code:
      global $DB;

            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,
               ]
            );
            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);
      // Authenticated user have read access to all nodes, as node list only contains elements
      // that user can read.
      $login = TU_USER;
      $pass  = TU_PASS;
      $this->login($login, $pass);

         'name' => 'Test group'

Fixed code:
use Glpi\CalDAV\Traits\CalDAVPrincipalsTrait;
   use CalDAVPrincipalsTrait;
            $groups_iterator = $this->getVisibleGroupsIterator();
            $users_iterator = $this->getVisibleUsersIterator();
use Glpi\CalDAV\Traits\CalDAVPrincipalsTrait;
use Sabre\DAVACL\IACL;
use Session;
   use CalDAVPrincipalsTrait;
      if (!($node instanceof IACL) || ($owner_path = $node->getOwner()) === null
          || !$this->canViewPrincipalObjects($owner_path)) {
         return $acl;
      }

<?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);
   }
}
      $login = 'tech';
      $pass  = 'tech';
         'name'    => 'Test group',
         'is_task' => 1,
      $this->login($login, $pass);

   /**
    * 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);
         }
      }
   }

    * @param string|null          $content_type