calendar · Календарь событий 26.125.100 Безопасность

Calendar 26.125.100: карточка события без полного доступа больше не выдаёт хоста, участников и раздел

5 мин чтения
Узнавайте о ломающих изменениях Битрикса раньше, чем они доедут до прода: свежие разборы — в канале @bxmax_updates

Обновление безопасности

Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.

В Calendar 26.125.100 карточка события и планировщик перестали отдавать пользователю без полного доступа хоста, автора, участников, данные раздела, ссылки и пользовательские поля. Режет данные новый класс Bitrix\Calendar\View\EventViewData, а ответы экшенов getViewEventSlider, getCalendarViewSliderParams и updatePlanner поменялись даже при полном доступе. PHP-сигнатуры и схема БД остались прежними, так что смотреть надо свой JS поверх этих экшенов, переопределённый шаблон calendar.view.slider и собственные эндпоинты, которые отдают события из CCalendarEvent::GetList.

Проверьте, что ваш код ждёт от карточки события

CalendarAjax::getViewEventSliderAction раньше рендерил компонент, даже если раздел события не нашёлся, и отдавал в section значение null. Теперь, если CCalendarSect::GetList не вернул активный раздел, экшен отвечает ошибкой SECTION_NOT_FOUND и пустым массивом.

Дальше запись, раздел и userIndex проходят через EventViewData::prepare(). Без полного доступа entry урезан до белого списка полей, userIndex пуст, от раздела остаются ID, CAL_TYPE, PERM и обнулённые NAME, COLOR и OWNER_ID, а entryUrl и sectionName приходят пустыми строками. При полном доступе sectionName, как и раньше, берётся прямо из $_REQUEST['section_name']. В параметры компонента добавлены isRestrictedEventView и userIndex.

При полном доступе userIndex тоже похудел. Раньше экшен отдавал весь \CCalendarEvent::getUserIndex(), теперь в индексе только автор, хост, создатель встречи, владелец личного календаря и участники этого события.

CalendarEventViewForm::getCalendarViewSliderParamsAction при ограниченном просмотре выходит раньше обычного через getRestrictedViewParams() и отдаёт фиксированный набор ключей:

  • meetingHost с ID => 0 и пустыми полями, пустые attendees;
  • description => null, location => '', showComments => false;
  • filesView, crmView, signedEvent и guestLinkUrl равны null, downloadIcsEnabled и guestLinkAvailable равны false;
  • canEditCalendar, canAttendeeEditCalendar и canDeleteEvent равны false;
  • parentId совпадает с ID самой записи, isRestrictedEventView => true.

В обычной ветке появился isRestrictedEventView => false, участников экшен собирает из уже суженного userIndex, а \CCalendarEvent::getEventPermissions() вызывается раньше, до EventViewData::prepare().

Если ваш JS разбирает этот ответ сам, ограниченную карточку отличайте по isRestrictedEventView.

Не рассчитывайте на hostId в updatePlanner

CalendarAjax::updatePlannerAction теперь подставляет cur_event_id в entryId до того, как вычисляет хоста. При entryId > 0 параметр hostId из запроса игнорируется. Хоста экшен берёт из MEETING_HOST события, прочитанного через \CCalendarEvent::GetById(). Если текущий пользователь не хост, событие ему не «своё» и полного доступа к нему нет, hostId обнуляется.

Без entryId, cur_event_id и hostId хост теперь подставляется сам. Для CAL_TYPE = user это $ownerId ?: $userId (раньше просто $ownerId), для остальных типов, кроме open_event, текущий пользователь, хотя раньше hostId здесь оставался нулём.

Если переопределяли шаблон calendar.view.slider

Шаблон .default компонента bitrix:calendar.view.slider при $arParams['isRestrictedEventView'] === true выставляет $arParams['UF'] = [] и выходит до вызова CCalendarEvent::GetEventUserFields. Своей разметки, кроме плашки «событие не найдено», у шаблона нет: данные карточки уходят в JSON-ответе экшена. userIndex он берёт из $arParams['userIndex'], а CCalendarEvent::getUserIndex() остался запасным вариантом.

Копию в шаблоне сайта, которая выводит данные события, обновление не тронет. Такая копия по-прежнему вызовет CCalendarEvent::GetEventUserFields(), а он читает пользовательские поля из базы по ID события в обход белого списка. Перенесите в начало копии ранний выход по isRestrictedEventView с обнулением $arParams['UF'].

Проверьте свои эндпоинты поверх CCalendarEvent::GetList

Утечку закрыли в контроллерах. CCalendarEvent::GetList прячет поля через ApplyAccessRestrictions, а тот по-прежнему вырезает только DESCRIPTION, LOCATION, REMIND, ATTENDEE_LIST, ATTENDEES_CODES, UF_CRM_CAL_EVENT и UF_WEBDAV_CAL_EVENT. MEETING_HOST, CREATED_BY, OWNER_ID, PARENT_ID, RRULE, IMPORTANCE, MEETING и RELATIONS остаются в записи. Индекс пользователей GetList собирает через getUsersDetails() (DISPLAY_NAME, URL, AVATAR) ещё до цикла с ApplyAccessRestrictions и копит в статическом self::$userIndex, то есть без учёта прав. Раздел оба контроллера читают через CCalendarSect::GetList с 'checkPermissions' => false. Если ваш эндпоинт отдаёт результат GetList и getUserIndex() как есть, он повторяет утечку, которую закрыл релиз.

Календарь теперь строит ответ по белому списку, и у себя можно делать так же. У EventViewData три публичных статических метода:

  • prepare(array $entry, array $section, array $permissions, array $userIndex, int $userId): array возвращает entry, section, userIndex и флаг restricted. Без полного доступа в записи остаются ID, NAME, даты и длительность, таймзоны и смещения, SECTION_ID/SECT_ID, CAL_TYPE и ACCESSIBILITY. Остальные поля отброшены, а 25 ключей, от OWNER_ID, CREATED_BY и MEETING_HOST до DESCRIPTION и RRULE, заданы заглушками. Права урезаются до view_time и view_title. Если view_title нет (у приватного личного события его нет всегда), NAME заменяется маской занятости.
  • canReadFullEntry(array $entry, array $permissions, int $userId): bool отвечает «нет», если IS_ACCESSIBLE_TO_USER === false. В остальных случаях «да», если событие «своё» или если оно не приватное личное и в правах есть view_full.
  • isOwnEntry(array $entry, int $userId): bool возвращает true автору, владельцу личного календаря и участнику встречи. Права метод не проверяет, поэтому дешёвый.

В updatePlannerAction первым вызывают isOwnEntry. Комментарий в коде объясняет, что права стоят «два чтения и пакетную проверку». В своём сервисе тот же порядок пригодится для списков, а карточку одного события можно собрать по той же схеме, что и getViewEventSliderAction:

        <?php declare(strict_types=1);

namespace Vendor\Agenda\Application\Service;

use Bitrix\Calendar\View\EventViewData;
use Bitrix\Main\Error;
use Bitrix\Main\Loader;
use Bitrix\Main\Result;

final class EventCardService
{
    public function getCard(int $eventId, int $userId): Result
    {
        $result = new Result();

        if (!Loader::includeModule('calendar')) {
            return $result->addError(new Error('Модуль calendar не установлен', 'CALENDAR_MODULE_MISSING'));
        }

        // календарь берёт запись через getEventForViewInterface() с датой экземпляра; для повторяющихся событий используйте его
        $event = \CCalendarEvent::GetById($eventId);
        if (!$event) {
            return $result->addError(new Error('Событие не найдено', 'EVENT_NOT_FOUND'));
        }

        // раздел читаем без проверки прав, как это делает календарь: при ограниченном просмотре NAME, COLOR и OWNER_ID обнулит prepare()
        $sections = \CCalendarSect::GetList([
            'arFilter' => ['ID' => (int)($event['SECTION_ID'] ?? 0), 'ACTIVE' => 'Y'],
            'checkPermissions' => false,
            'getPermissions' => true,
        ]);
        $section = $sections[0] ?? null;
        if (!is_array($section)) {
            return $result->addError(new Error('Раздел события не найден', 'SECTION_NOT_FOUND'));
        }

        $view = EventViewData::prepare(
            $event,
            $section,
            \CCalendarEvent::getEventPermissions($event, $userId),
            \CCalendarEvent::getUserIndex(),
            $userId,
        );

        // entry, section, userIndex и флаг restricted для фронта
        return $result->setData($view);
    }

    /**
     * @param list<array> $events строки из \CCalendarEvent::GetList()
     * @return array<int, int> ID события => ID хоста, 0 если хоста показывать нельзя
     */
    public function getVisibleHostIds(array $events, int $userId): array
    {
        $hosts = [];
        foreach ($events as $event) {
            // права стоят дорого, поэтому сначала дешёвая проверка «своего» события
            $visible = EventViewData::isOwnEntry($event, $userId)
                || EventViewData::canReadFullEntry(
                    $event,
                    \CCalendarEvent::getEventPermissions($event, $userId),
                    $userId,
                );

            $hosts[(int)$event['ID']] = $visible ? (int)($event['MEETING_HOST'] ?? 0) : 0;
        }

        return $hosts;
    }
}

    

Не берите название события из pull

Комментарий в view-event-slider.js говорит, что поля pull-сообщения не режутся по правам получателя. Отправку pull релиз не менял, поменялась только карточка ViewEventSlider. При ограниченном просмотре она игнорирует pullData.NAME и после перезапроса берёт name из ответа сервера. Строка this.name = newData.name появилась впервые, до этого название из ответа сервера в карточке не обновлялось вовсе.

В урезанной записи PARENT_ID заменён на собственный ID, так что связь с родительской встречей из записи не видна. Поэтому сопоставление pull по PARENT_ID в JS дополнили сравнением по ID в новом методе isPullOfCurrentEntry(fields). Ограниченную карточку JS узнаёт по параметру isRestrictedEventView.

Если ваш интерфейс подписан на pull календаря, берите из сообщения только факт изменения, а название и остальные поля перезапрашивайте через контроллер. Код отправки подтверждает комментарий: Util::addPullEvent() шлёт edit_event с полными fields ещё и в тег calendar-planner-<ID>, а updatePlannerAction подписывает на этот тег каждого, кто открыл планировщик с этим пользователем. На стенде это не проверяли.

Мелочи и находки

  • Маску названия в CCalendarEvent::ApplyAccessRestrictions и GetAccessibilityForUsers строит новый публичный статический CCalendarEvent::getBusyMaskedName(array $event): string, он возвращает [<фраза доступности>]. При пустом ACCESSIBILITY подставляется BUSY, раньше читался ключ EC_ACCESSIBILITY_ без суффикса. На практике это касается только GetAccessibilityForUsers, потому что ApplyAccessRestrictions и раньше заранее ставил 'busy'.
  • В EventViewData есть приватная копия getBusyMaskedName, идентичная публичной. По докблоку класс держит правило у себя и не полагается на вызывающий код.
  • MEETING и RELATIONS в урезанной записи заданы как new \stdClass(), поэтому в JSON приходят пустым объектом {}.
  • Если при ограниченном просмотре в updatePlannerAction пришли пустые codes, экшен добавит код 'U' . $hostId с нулевым хостом, то есть U0. Текущего пользователя он в список не добавит, потому что codes уже не пуст (если не передан add_cur_user_to_list = 'Y'). Как это выглядит в интерфейсе, по диффу не видно.
  • URL создателя встречи шаблон по-прежнему строит из $meetingCreator["PATH_TO_USER"], то есть путь берётся из массива пользователя вместо $arParams. Релиз добавил к нему только ?? '', так же как к PATH_TO_USER в $arParams.
  • Изменения в bitrix/components/bitrix/calendar.view.slider/... и bitrix/js/calendar/eventviewform/... побайтно совпадают с копиями в bitrix/modules/calendar/install/.... Бандлы dist/*.bundle.js, *.min.js и *.map пересобраны из того же исходника.
  • Deprecated в релизе нет, схема БД не менялась. Версия выросла с 26.125.0, VERSION_DATE у новой 2026-09-24.

Новая версия вышла — разбор уже в Telegram

В канале @bxmax_updates публикуем каждый свежий разбор: что сломается, что устарело и что появилось в API. Узнаете до того, как обновите прод.

Подписаться

Читайте дальше

calendar 26.200.0 Ломающее Свежее

Calendar 26.150.0–26.200.0: двойная бронь переговорки стала ошибкой, а форма бронирования больше не отдаёт чужую занятость

Календарь вышел двумя версиями, 26.150.0 и 26.200.0. В 26.150.0 закрыли утечку в анонимном экшене бронирования CRM-формы, через который любой посетитель мог узнать занятость и имена сотрудников и ресурсов портала. В 26.200.0 занятость переговорок проверяется по умолчанию в `CCalendar::SaveEvent`, RE…

1 мин
calendar 26.100.0 Безопасность Свежее

Calendar 26.100.0: почтовые приглашения и calendar.section.update берут хоста, тип и владельца из базы

В Calendar 26.100.0 REST-метод `calendar.section.update` берёт тип и владельца секции из базы, а обработчик входящих iCal-приглашений ищет событие только среди встреч получателя и при обновлении встречи читает её хоста из базы. В участники встречи теперь можно звать команды из модуля `humanresources…

4 мин
calendar 26.125.0 Рутинное Свежее

Calendar 26.125.0: компактная форма события вне календаря

Экшен `calendar.api.calendarajax.getStandaloneCompactFormData` собирает секции для формы заново. Обычному пользователю он гарантирует секцию для сохранения и при необходимости создаёт личный календарь. Коллаберу отдаёт только секции его коллабов с правом записи, экстранет-пользователю и коллаберу бе…

1 мин

calendar 26.0.100: почему у людей пропадали встречи при синхронизации с Google и iCloud

Маленький релиз с большими последствиями — исправлена потеря данных при импорте календаря. Если внешний сервис отдавал неполную страницу событий, Битрикс считал недостающие экземпляры удалёнными и вычищал их у себя. Публичного API изменение не трогает, но поставить его стоит всем, у кого работает си…

3 мин
Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.
AI Домовой

AI Домовой История

на связи

пишет…

Можно закрыть чат и перейти на другую страницу. Когда ответ будет готов, на кнопке Домового появится отметка.

Нет истории чатов
AI Домовой

Нужна авторизация

Войдите, чтобы задавать вопросы AI Домовому.

Войти