Bizproc 26.1050.0: ID бизнес-процесса проверяется по формату, а Starter его больше не пропускает
Обновление безопасности
Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.
В bizproc 26.1050.0 заранее заданный ID экземпляра бизнес-процесса (PreGeneratedWorkflowId) теперь проверяется по формату в четырёх местах, а из параметров, которые собирает Bitrix\Bizproc\Starter\Parameters, вырезается совсем. Тем же релизом в нескольких сырых SQL-запросах модуля убрали склейку WORKFLOW_ID строками. Для тех, кто пишет свои активности, есть новый трейт подписки на события других модулей и событие, через которое можно подложить ядру свои шаблоны нод. Сигнатуры и схема БД не менялись.
Проверьте, откуда берётся PreGeneratedWorkflowId
CBPDocument::PARAM_PRE_GENERATED_WORKFLOW_ID (строка 'PreGeneratedWorkflowId') позволяет передать в запуск заранее известный ID процесса. Раньше ядро брало любое переданное значение. Теперь формат проверяет новый CBPRuntime::isValidWorkflowId():
return is_string($workflowId) && preg_match('/^[0-9a-f]{13}\d\.\d{8}$/D', $workflowId) === 1;
Это ровно формат uniqid('', true), который возвращает CBPRuntime::generateWorkflowId(). В PHPDoc метод назван единственным источником правды о формате, менять его велено только вместе с генератором.
CBPDocument::startWorkflow()иstartDebugWorkflow()молча заменяют невалидный ID на сгенерированный;CBPRuntime::createWorkflow()иcreateDebugWorkflow()делают то же и вдобавок удаляют ключ из параметров процесса;CBPStateService::addWorkflow()бросаетException("InvalidWorkflowId");Bitrix\Bizproc\Starter\Parameters::getValues()вырезает ключ всегда, с комментарием в коде «the client must never choose the workflow instance id».
Если ваш код передаёт в запуск собственный ID другого формата (UUID, ID из внешней системы), процесс получит не тот ID, который вы сохранили у себя, и ядро об этом не сообщит. Если вы зовёте CBPStateService::addWorkflow() напрямую со своими ID, получите исключение.
Заранее известный ID теперь берите у ядра:
<?php declare(strict_types=1);
use Bitrix\Main\Loader;
Loader::requireModule('bizproc');
$workflowId = \CBPRuntime::generateWorkflowId();
// сохраните $workflowId у себя до запуска, формат гарантированно пройдёт проверку
$errors = [];
\CBPDocument::startWorkflow(
$templateId,
$documentId, // комплексный ID документа: [модуль, класс документа, ID]
[\CBPDocument::PARAM_PRE_GENERATED_WORKFLOW_ID => $workflowId],
$errors,
);
Если ID процесса приходит к вам из запроса, прогоняйте его через \CBPRuntime::isValidWorkflowId() до того, как он попадёт в выборку или SQL.
Не склеивайте ID в SQL
В том же релизе в модуле переписали несколько запросов, куда WORKFLOW_ID попадал склейкой строк:
ClearFilterAgent::clear()иWorkflowUserTable::onDocumentDelete()удаляют записи черезdeleteByFilter(['@WORKFLOW_ID' => ...])вместоDELETE ... IN('...')изimplode; вWorkflowFilterTableиWorkflowUserTableподключилиDeleteByFilterTrait;CreateUserFilterStepperсобираетINSERT IGNORE ... WHERE ws.ID IN (?@)черезSqlExpression;- в
SchedulerEventTable,WorkflowDurationStatTable,WorkflowInstanceTableиWorkflowStateTableимена таблиц в сырых запросах экранируетquote(), а неforSql().
Убрали склейку не везде. SchedulerEventTable::deleteBySubscription() и deleteByWorkflow() по-прежнему строят WHERE WORKFLOW_ID = '{$workflowId}' со значением через forSql(), в них поменялось только экранирование имени таблицы.
Проверка формата ID и уход от склейки пришли в одном релизе и похожи на закрытие одной цепочки, в которой выбранный клиентом ID мог доехать до сырого SQL. Это наш вывод из характера правок.
Тот же приём для таблицы вашего модуля:
<?php declare(strict_types=1);
use Bitrix\Main\Application;
use Bitrix\Main\DB\SqlExpression;
// ?@ на пустом списке бросает ArgumentException
if ($workflowIds !== []) {
Application::getConnection()->query(new SqlExpression(
'DELETE FROM ?# WHERE WORKFLOW_ID IN (?@)',
'b_vendor_workflow_log',
$workflowIds,
));
}
Подпишите активность на события другого модуля
Новый трейт Bitrix\Bizproc\Activity\Mixins\ExternalEventSubscriptionTrait берёт на себя подписку активности через SchedulerService на события чужого модуля, необязательный таймаут и проверку «моё ли это событие». Все методы, кроме приватного помощника extractFirstStringParam(), объявлены protected:
subscribeOnExternalEvents($moduleId, $eventNames, $hash, $timeoutSeconds = 0)подписывает на каждое событие из списка, при положительном таймауте ставит ещё и таймер;unsubscribeFromExternalEvents($moduleId, $eventNames)снимает и то и другое;isExternalEventIrrelevant($eventParams, $eventNames)отсекает события для закрытой активности или активности без подписки, чужие имена и чужой хеш; срабатывание таймера проходит, если активность ещё не закрыта и подписка есть;isExternalEventTimeout(),extractEventObject(),extractEventError(),switchExternalEventSubscriptionTrait()для переподписки на новый хеш иreInitializeEventSubscription()для сброса состояния.
Хеш сверяется с первым строковым позиционным аргументом события, и именно там ваше событие должно передавать ключ. Набросок активности, которая ждёт подтверждения оплаты от модуля vendor.payments:
<?php declare(strict_types=1);
use Bitrix\Bizproc\Activity\Mixins\ExternalEventSubscriptionTrait;
class CBPVendorWaitPaymentActivity extends CBPActivity implements IBPActivityExternalEventListener
{
use ExternalEventSubscriptionTrait;
private const MODULE = 'vendor.payments';
private const EVENTS = ['OnPaymentConfirmed'];
public function __construct($name)
{
parent::__construct($name);
$this->arProperties = ['Title' => '', 'PaymentId' => '', 'IsPaid' => 'N'];
}
public function execute()
{
$this->subscribeOnExternalEvents(self::MODULE, self::EVENTS, (string)$this->PaymentId, 3600);
return CBPActivityExecutionStatus::Executing;
}
public function onExternalEvent($arEventParameters = [])
{
if ($this->isExternalEventIrrelevant($arEventParameters, self::EVENTS)) {
return;
}
$this->unsubscribeFromExternalEvents(self::MODULE, self::EVENTS);
$this->IsPaid = $this->isExternalEventTimeout($arEventParameters) ? 'N' : 'Y';
$this->workflow->closeActivity($this);
}
}
В этом релизе ядро трейт ещё нигде не использует, в диффе он лежит сам по себе.
Подложите свои шаблоны нод
Bitrix\Bizproc\Public\Service\Template\NodesInstallerService::syncSection() раньше ставил шаблоны только из собственного каталога nodes/<раздел> модуля и выходил, если каталога нет. Теперь он дополнительно шлёт событие bizproc:onGetExternalNodesDirs с параметром sectionId и забирает из успешных ответов массив путей dirs. Несуществующие каталоги пропускаются, каждый подкаталог раздела ставится тем же installFromDir(), что и штатные шаблоны.
<?php declare(strict_types=1);
namespace Vendor\Module\Internals\Integration\Bizproc;
use Bitrix\Main\Event;
use Bitrix\Main\EventResult;
final class NodesDirsHandler
{
public static function onGetExternalNodesDirs(Event $event): EventResult
{
// /local/modules/vendor.module/nodes/<sectionId>
$dir = dirname(__DIR__, 4) . '/nodes/' . $event->getParameter('sectionId');
return new EventResult(EventResult::SUCCESS, ['dirs' => [$dir]], 'vendor.module');
}
}
Регистрируйте обработчик в install/index.php модуля через EventManager::getInstance()->registerEventHandler('bizproc', 'onGetExternalNodesDirs', 'vendor.module', NodesDirsHandler::class, 'onGetExternalNodesDirs') и снимайте при удалении.
Мелочи и находки
- Метод трейта
switchExternalEventSubscriptionTrait()назван с суффиксомTrait, остальные методы трейта без него. - В
HiddenAiAgentsRegistryпоявился третий скрытый отовсюду AI-агент,bitrix_crm_call_assessment, рядом сbitrix_ai_day_plannerиbitrix_booking_ai_call. - В
install/version.phpубрали пустую строку после<?phpи висячую запятую. Дата сборки — 30 июля 2026.
Что делать
- Найдите в своём коде
PARAM_PRE_GENERATED_WORKFLOW_IDиPreGeneratedWorkflowId. Если ID генерируете сами, переходите наCBPRuntime::generateWorkflowId(). - Проверьте прямые вызовы
CBPStateService::addWorkflow(): невалидный ID теперь бросает исключение. - ID процесса, пришедший из запроса, проверяйте
CBPRuntime::isValidWorkflowId(). - Если пишете активность, которая ждёт событий другого модуля, посмотрите на
ExternalEventSubscriptionTraitвместо самописной подписки.