AI 26.1300.0–26.1500.0: закрыта XSS в имени роли копилота, ответ из кеша больше не даёт ложный сбой очереди
Обновление безопасности
Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.
В коробочный Битрикс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смотрят на флаг bizprocAiAgentSectionFlag(флаги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()
|