ai 26.1500.0 Безопасность

AI 26.1300.0–26.1500.0: закрыта XSS в имени роли копилота, ответ из кеша больше не даёт ложный сбой очереди

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

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

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

В коробочный Битрикс24 пришли три версии модуля ai: 26.1300.0, 26.1400.0 и 26.1500.0. Обновиться стоит ради 26.1500.0. В нём меню копилота перестало вставлять имя пользовательской роли в страницу как HTML, а такие роли расшаривают на отделы и группы. В 26.1400.0 очередь ИИ-запросов перестала слать ложный onQueueJobFail на ответы из кеша, а 26.1300.0 коробку почти не задевает: он учит пикер показывать технический лимит Vibe+ и добавляет поле в событие аналитики.

Новое

Ответ из кеша больше не превращается в сбой (26.1400.0)

Часть промптов копилот отдаёт из кеша результатов, и запрос в облако не уходит. Задача в b_ai_queue при этом всё равно создавалась и оставалась висеть. Через TTL, по умолчанию четыре часа, агент очистки признавал её просроченной: слал событие ai:onQueueJobFail с ошибкой HASH_EXPIRED, такой же пуш в браузер пользователя и возвращал списанный запрос в лимит. Теперь задача удаляется сразу. Ложного сбоя нет, а ответ из кеша остаётся списанным.

Если вы считаете сбои по событиям очереди, до обновления в статистику попадали и удачные ответы из кеша. Событие приходит по всем ИИ-задачам портала, так что свои надо отбирать по контексту:

        <?php declare(strict_types=1);

use Bitrix\AI\Engine\IEngine;
use Bitrix\Main\Diag\Logger;
use Bitrix\Main\Error;
use Bitrix\Main\Event;
use Bitrix\Main\EventManager;

EventManager::getInstance()->addEventHandler(
    'ai',
    'onQueueJobFail',
    static function (Event $event): void {
        $engine = $event->getParameter('engine');
        if (!$engine instanceof IEngine || $engine->getContext()->getModuleId() !== 'vendor.assistant') {
            return;
        }

        $error = $event->getParameter('error');

        // до 26.1400.0 сюда через TTL попадал и ответ из кеша с кодом HASH_EXPIRED
        Logger::create('vendor.assistant.queue')?->warning('AI queue job {hash} failed: {code}', [
            'hash' => (string)$event->getParameter('queue'),
            'code' => $error instanceof Error ? (string)$error->getCode() : 'UNKNOWN',
        ]);
    },
);

    

В той же версии очередь научилась склеивать повторы. Если живая задача с тем же отпечатком и тем же телом запроса уже есть, новая попытка берёт её hash, лимит за повтор сразу возвращается, а onQueueJobExecute и onQueueJobFail придут по этому hash один раз. Склейка есть только в базовом Engine\Engine::completionsInQueue(). В коробке на нём работают Triton\Classify и ваши наследники Engine\Engine, у облачных и REST-движков повторы по-прежнему идут отдельно. Те же движки шлют в очередь заголовок X-Cuet-Feature с moduleId вашего Context, а с 26.1500.0 и с его contextId.

Что заметят пользователи

  • Поиск в диалоге ролей со скобкой ( или [ раньше падал с исключением, теперь ищет (26.1400.0).
  • Библиотека промптов больше не прячет имя и аватар сотрудника или отдела, если его id совпал с id скрытой группы (26.1500.0).
  • Роль, открытая на редактирование, перестала держать удалённые группы и отделы из-за такого же совпадения id (26.1500.0).

Находка: option_triggers.php для своих опций

В 26.1500.0 у ai появился option_triggers.php. Через него модуль ловит запись Y в свои опции и решает, считать ли её выпуском фичи для аналитики. Механизм давний, такие файлы есть у main и disk. При первом Option::set() модуля за хит main подключает modules/<moduleId>/option_triggers.php через Loader::getLocal(), поэтому файл подхватится и из /local/modules/. Регистрировать обработчик в b_module_to_module и снимать его при удалении модуля не нужно.

Файл подключается внутри Option::loadTriggers() и видит переменные метода, поэтому в ai весь код спрятан в замыкание. Событие OnAfterSetOption_<имя> несёт только значение, без модуля, и опция с тем же именем у чужого модуля его тоже вызовет. Надёжнее слушать общее OnAfterSetOption и проверять moduleId. Имена модуля и опции приходят в нижнем регистре.

        <?php declare(strict_types=1);

// /local/modules/vendor.crmsync/option_triggers.php

use Bitrix\Main\Application;
use Bitrix\Main\Event;
use Bitrix\Main\EventManager;

EventManager::getInstance()->addEventHandler(
    'main',
    'OnAfterSetOption',
    static function (Event $event): void {
        // после подключения файла обработчик слышит опции всех модулей до конца хита
        if ($event->getParameter('moduleId') !== 'vendor.crmsync') {
            return;
        }

        if (in_array($event->getParameter('name'), ['api_url', 'api_token'], true)) {
            Application::getInstance()->getTaggedCache()->clearByTag('vendor_crmsync_client');
        }
    },
);

    

Что сломается

Замените удалённые методы DepartmentService и GroupService (26.1500.0)

Интеграционные сервисы ai больше не выгружают все отделы и группы портала, а проверяют только переданные id:

  • Bitrix\AI\Integration\Intranet\DepartmentService::getDepartments() заменил getExistingDepartmentIds(array $departmentIds);
  • Bitrix\AI\Integration\Socialnetwork\GroupService::getAllGroupCodes() заменил getExistingGroupIds(array $groupIds);
  • GroupService::getNotVisibleGroupListInKeys() заменил getNotVisibleGroupIds(array $groupIds), он возвращает список id, а не массив с id в ключах.

Полной замены у getDepartments() нет, новый метод отвечает только за переданные id. getExistingGroupIds() без модуля socialnetwork бросает LoaderException вместо пустого массива, потому что пустой ответ стёр бы доступы групп. Конструкторы теперь требуют DepartmentRepository и GroupRepository, так что new DepartmentService() упадёт с ArgumentCountError. Собрать сервис с зависимостями может контейнер модуля:

        <?php declare(strict_types=1);

use Bitrix\AI\Container;
use Bitrix\AI\Integration\Socialnetwork\GroupService;
use Bitrix\Main\Loader;

Loader::requireModule('ai');

/** @var GroupService $groupService */
$groupService = Container::init()->getItem(GroupService::class);

$aliveIds = $groupService->getExistingGroupIds([12, 40, 77]); // без socialnetwork бросит LoaderException
$hiddenIds = $groupService->getNotVisibleGroupIds($aliveIds);

    

Задевает это немногих, потому что в ядре эти сервисы вызывает только сам ai. Проверить свой код: grep -rnE 'DepartmentService|GroupService' local/.

Проверьте наследников

В 26.1500.0 конструктор SharePrompt\Service\ShareService получил обязательный шестой параметр CategoryService, а у ShareRole\Service\GridRole\GridRoleService пропали protected-методы getCodeForGroup(), getCodeForDepartment(), getExistingGroupCodes() и getExistingDepartmentCodes(). В 26.1400.0 константа Engine\Engine::PARAM_CONSUMPTION_ID стала public, и наследник, который переобъявил её как protected, PHP не загрузит.

Обновитесь, если роли создают пользователи (26.1500.0)

В меню копилота есть пункт с текущей ролью. До 26.1500.0 её имя стояло в шаблоне Tag.render текстом элемента, а такую подстановку Tag.render не экранирует и разбирает как разметку: теги и обработчики вроде onerror создаются настоящими. Сервер отдаёт имя без экранирования. У пользовательской роли это название, которое ввёл автор, а роль расшаривают на пользователей, отделы и группы. Выходит хранимая XSS, которая срабатывает в меню копилота у каждого, кто выбрал эту роль. В гриде библиотеки ролей имя экранируется, поэтому там такая роль выглядит безобидно.

Теперь имя ставится через textContent. Так же стоит класть пользовательский текст и в своих шаблонах на Tag.render. До обновления проверьте, не завелись ли роли с разметкой в названии. Имя роли лежит в двух местах: переводы в b_ai_role_translate_name и DEFAULT_NAME в b_ai_role. DEFAULT_NAME видят пользователи с другим языком интерфейса, и при правке роли он не обновляется.

        SELECT r.ID, r.AUTHOR_ID, r.DEFAULT_NAME, n.LANG, n.TEXT
FROM b_ai_role r
LEFT JOIN b_ai_role_translate_name n ON n.ROLE_ID = r.ID
WHERE r.IS_SYSTEM = 'N'
  AND (r.DEFAULT_NAME LIKE '%<%' OR n.TEXT LIKE '%<%');

    

БД: проверьте индекс на b_ai_queue (26.1400.0)

В install/migrations/tables.php у b_ai_queue появился индекс IX_B_CACHE_HASH по CACHE_HASH, на него опирается поиск живой задачи при склейке повторов. Файл описывает чистую установку. Как индекс попадёт на работающий портал, по коду модуля не видно: скрипт обновления в папке модуля не остаётся. На MySQL проверить можно так:

        SHOW INDEX FROM b_ai_queue WHERE Key_name = 'IX_B_CACHE_HASH';

    

В 26.1300.0 и 26.1500.0 схема не менялась.

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

  • Триггеры AI-агентов aiagentstarttrigger и aiagentrestarttrigger смотрят на флаг bizproc AiAgentSectionFlag (флаги Main\Config\Feature) вместо опции bizproc:feature_ai_agents, как setuptemplateactivity в bizproc 26.1400.0, но в коробке без модуля aiassistant они скрыты и так (26.1500.0).
  • ai научился добавлять к категории tasks_results доступные пользователю несистемные промпты «Задач» (26.1500.0). В коробке этого пока не видно: редактор результата задачи вызывает копилот с категорией system.
  • Проверка метки в ContextHeaders получила модификатор D (26.1500.0). Без него $ в PCRE пропускает перевод строки в конце значения. В ai это страховка, метку и так чистит FeatureTag. Но main сам такой хвост не ловит: HttpHeaders::validateValue() проверяет значение регуляркой без D. Если проверяете заголовки регуляркой ^…$, добавьте D или пишите \z.
  • Services\MarkdownToBBCodeTranslationService::convert() больше не падает с TypeError, когда PCRE сдаётся на формуле, и оставляет ответ как был (26.1500.0). Через этот метод ответы ИИ переводят в BBCode сам ai, imbot и imopenlines.

Прошлый разбор модуля: AI 26.1200.0.

Устаревшие и удалённые API в этой версии

Символ Статус Чем заменять
Bitrix\AI\Integration\Intranet\DepartmentService::getDepartments() Удалено Bitrix\AI\Integration\Intranet\DepartmentService::getExistingDepartmentIds()
Bitrix\AI\Integration\Socialnetwork\GroupService::getAllGroupCodes() Удалено Bitrix\AI\Integration\Socialnetwork\GroupService::getExistingGroupIds()
Bitrix\AI\Integration\Socialnetwork\GroupService::getNotVisibleGroupListInKeys() Удалено Bitrix\AI\Integration\Socialnetwork\GroupService::getNotVisibleGroupIds()

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

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

Подписаться

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

ai 26.1200.0 Безопасность Свежее

AI 26.1200.0: запись истории ИИ-запросов через REST включает только администратор

В модуле `ai` коробочного Битрикс24 версии 26.1200.0 REST-методы `ai.history.enable` и `ai.history.disable` пускают только администратора. Они включают и выключают запись истории всех ИИ-запросов портала, а проверки администратора в них до этой версии не было. Ещё модуль перестал подключаться на лиц…

3 мин
ai 26.1100.0 Заметное Свежее

AI 26.1100.0: срок жизни задачи очереди ИИ-запросов задаётся на запрос

В модуле `ai` коробочного Битрикс24 версии 26.1100.0 срок жизни задачи очереди ИИ-запросов больше не зашит в четыре часа. Его задают на запрос через `Engine::setQueueJobTtl()`, момент истечения пишется в новую колонку `b_ai_queue.EXPIRE_DATE`, а просроченную задачу `QueueJob::createFromHash()` тепер…

4 мин
ui 26.693.0 Заметное Свежее

UI 26.693.0: загрузчик называет причину отказа, а вьювер прячет кнопку скачивания

В ui 26.693.0 загрузчик файлов перед сохранением ещё раз проверяет временный файл и при отказе отдаёт текст ошибки из `CFile::CheckFile`, а при превышении дисковой квоты кладёт в ошибку причину `disk_quota_exceeded`. Вьювер теперь прячет кнопку скачивания у отдельного файла, а `FeaturePromoter.show(…

3 мин
socialnetwork 26.250.0 Рутинное Свежее

Socialnetwork 26.250.0: комментарий о том, как дочерние чаты проекта попадают в папку «Все»

В socialnetwork 26.250.0 код не менялся, проверять после обновления нечего. В двух внутренних обработчиках, `SendChildAttachedMessage` и `SendChildDetachedMessage`, появился одинаковый docblock о том, как дочерний чат проекта попадает в папку «Все» мессенджера. Push `chatUpdate` только убирает строк…

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

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

на связи

пишет…

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

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

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

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

Войти