Report 26.200.0: белый список агрегатов против SQL-инъекции и самопочинка дат в фильтрах
Обновление безопасности
Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.
В модуле конструктора отчётов report закрыта SQL-инъекция через агрегатную функцию из настроек отчёта, которая попадала в запрос без проверки. Ещё одна правка чинит отчёты, которые падали при каждом открытии из-за даты с годом вне диапазона БД, и ради этого report.view может сам переписать b_report.SETTINGS. Сигнатуры совместимы, схема БД не менялась.
Проверьте отчёты с нестандартными агрегатами
CReport::prepareSelectViewElement() и CReport::appendHrefSelectElements() вставляли в SQL значение aggr из настроек колонки. По комментарию в коде, aggr может прийти из импортированных или вручную собранных настроек в обход проверки в интерфейсе, и тогда это SQL-инъекция. Теперь значение сверяется с белым списком:
protected static $allowedAggrFuncs = [
'SUM', 'AVG', 'MIN', 'MAX', 'COUNT_DISTINCT', 'GROUP_CONCAT'
];
Если aggr не пустой и в список не входит, обе функции бросают BXUserException с фразой REPORT_INVALID_AGGREGATION_FUNCTION. Отчёт с таким значением в настройках после обновления не построится.
Найти такие отчёты заранее можно новым CReport::isAllowedAggregationFunction(). Смотреть надо в двух местах. Первое — aggr самой колонки, второе — aggr элементов href['elements'], их проверяет appendHrefSelectElements().
<?php declare(strict_types=1);
use Bitrix\Main\Loader;
use Bitrix\Report\ReportTable;
Loader::requireModule('report');
$rows = ReportTable::query()
->setSelect(['ID', 'TITLE', 'SETTINGS'])
->fetchAll();
foreach ($rows as $row)
{
$settings = unserialize((string)$row['SETTINGS'], ['allowed_classes' => false]);
foreach ((array)($settings['select'] ?? []) as $column)
{
$candidates = [$column['aggr'] ?? ''];
foreach ((array)($column['href']['elements'] ?? []) as $hrefElement)
{
$candidates[] = $hrefElement['aggr'] ?? '';
}
foreach ($candidates as $aggr)
{
// то же условие, что в ядре: непустое и не из белого списка
if (!empty($aggr) && !CReport::isAllowedAggregationFunction($aggr))
{
printf("#%d %s: aggr=%s\n", $row['ID'], $row['TITLE'], var_export($aggr, true));
}
}
}
}
Сам список отдаёт CReport::getAllowedAggregationFunctions().
Отчёт с битой датой больше не падает при каждом открытии
Судя по комментариям в коде, дата с годом за пределами диапазона БД (например, пятизначным) превращалась в пустой литерал даты в SQL и роняла отчёт. Такая дата сохранялась в настройки отчёта или в персональные параметры просмотра, и отчёт падал на каждом открытии. Сохранённый просмотр к тому же редиректил на те же сломанные параметры.
Как теперь:
report.constructне даёт сохранить период, если в нём год вне диапазона БД или строка, которая не разбирается как дата. В условиях фильтра по полю даты отклоняется год вне диапазона и строка, которую не разбирает дажеstrtotime(), поэтому относительные значения вродеtodayи-1 weekпроходят. В обоих случаях форма показываетREPORT_ERR_FILTER_DATE_INVALID, условиеBETWEENпроверяется по каждой границе.report.viewсохраняет свежий фильтр в персональные параметры только после валидации. Раньше он писал их до проверки.- Если битая дата лежит в сохранённом периоде отчёта, а пользователь может редактировать отчёт и отчёт не стандартный (
MARK_DEFAULT), период сбрасывается на текущий месяц, настройки сохраняются, пользователь видит сообщениеREPORT_FILTER_DATE_FIXED. Остальные получаютREPORT_ERR_FILTER_DATE_LOCKEDс просьбой обратиться к владельцу. - Битое условие фильтра из сохранённых настроек выбрасывается, а определение отчёта пересохраняется из снимка настроек, снятого до всех runtime-преобразований. Права и сообщения те же, что для периода.
- Сломанный персональный просмотр удаляется только у текущего пользователя и только для текущего шаблона.
- Строка в поле даты, которая не разбирается ни как абсолютная, ни как относительная дата, раньше молча превращалась в текущую дату. Теперь это ошибка для свежего ввода и повод починить сохранённое значение.
- Если при выгрузке в Excel случилась такая ошибка, файл не отдаётся. Раньше пользователь скачал бы пустой «успешный» отчёт.
Открытие отчёта теперь может изменить его определение в b_report.SETTINGS. Внешний код, который держит копию этих настроек или сравнивает их с эталоном, после такого открытия увидит другой период или меньше условий фильтра.
Возьмите готовый классификатор дат
Для проверки дат в CReport добавлены публичные статические методы:
classifyFilterDate($rawValue, $format = false)возвращает['type' => ..., 'timestamp' => ...], где тип — одна из константFILTER_DATE_EMPTY,FILTER_DATE_VALID,FILTER_DATE_INVALIDилиFILTER_DATE_UNRESOLVED;isFilterDateInvalid(): дата разобрана, но в БД не влезет;isFilterDateUnusable(): то же плюс строка, которая не разбирается и как относительная дата;isTimestampStorable($timestamp): сможет лиBitrix\Main\Type\DateTimeпредставить эту метку;periodInputHasInvalidDate($type, $rawFrom, $rawTo, $format = false)иperiodHasUnstorableDate($period)проверяют период из запроса и из сохранённых настроек;updateSettings($ID, $settings, $viewParamsUserId = null)обновляет только колонкуSETTINGS, не трогая заголовок, описание иMARK_DEFAULT. Даты он сам не проверяет, а после записи зовётclearViewParams($ID, $viewParamsUserId), так что без третьего аргумента сохранённые просмотры отчёта пропадут у всех пользователей.
Если у вас свой фильтр по датам поверх отчётов, классификатор можно взять как есть:
<?php declare(strict_types=1);
use Bitrix\Main\Loader;
Loader::requireModule('report');
$type = CReport::classifyFilterDate($rawDate)['type'];
$hint = match ($type) {
CReport::FILTER_DATE_EMPTY => 'дата не задана',
CReport::FILTER_DATE_VALID => 'можно фильтровать',
CReport::FILTER_DATE_INVALID => 'дата разобрана, но в БД не влезет',
CReport::FILTER_DATE_UNRESOLVED => 'не абсолютная дата, возможно today или -1 week',
};
У CReport::clearViewParams($id, $onlyUserId = null) появился второй параметр. Без него метод, как и раньше, чистит сохранённые просмотры отчёта у всех пользователей, а с ним только у одного:
CReport::clearViewParams($reportId, onlyUserId: $userId);
Мелочи и находки
- Комментарии в
report.view/component.phpтрижды объясняют, чтоclearViewParams()снёс бы просмотры у всех пользователей, поэтому компонент зовётCUserOptions::DeleteOption()напрямую. МетодclearViewParams()в том же релизе научился чистить просмотры одного пользователя, но снёс бы их по всем шаблонам, а компоненту нужен только текущий. - В
report.view/component.phpиз 315 добавленных строк 115 занимают комментарии. - Новые фразы
REPORT_*добавлены в lang-файлы компонентовreport.viewиreport.construct. Lang-файла дляclasses/general/report.phpв изменениях нет. updateSettings()написан на старом$DB->PrepareUpdateиQueryBind, без ORM.- В
install/version.phpмассив переписан на короткий синтаксис. Дата версии 26.200.0 — 1 сентября 2026. - Deprecated, удалённых методов и изменений схемы БД нет.
Что делать
- Прогоните проверку
aggrпо своим отчётам, особенно по импортированным и созданным программно. - Если отчёты создаются или правятся кодом, проверьте даты в
periodиfilterна год вне диапазона БД и нераспознаваемые строки. - Предупредите тех, кто редактирует отчёты: при открытии отчёта с битой датой период может сброситься на текущий месяц.