Sale 26.500.0: пачка защитных правок от личного кабинета до импорта местоположений
Обновление безопасности
Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.
Sale 26.500.0 почти целиком состоит из точечных защитных правок: CSRF-проверки в ajax-обработчиках личного кабинета, сверка платежа с платёжной системой, ограничения на пути и адреса при импорте местоположений, экранирование в админке и печатных формах. Заодно установщик модуля переехал с install.sql на миграции ядра, а в описании схемы для новых установок шесть денежных колонок платежей и отгрузок получили точность decimal(26,8). ALTER для существующих баз в диффе нет. Сломаться после обновления могут кастомные шаблоны личного кабинета, фронтенд оплаты, своя касса и скрипты импорта местоположений.
Проверьте запросы к ajax.php личного кабинета
В ajax.php компонентов sale.order.payment.change, sale.personal.order.detail и sale.personal.order.list исправили одну логическую операцию. Раньше проверка в начале обработчика выглядела так:
if (!check_bitrix_sessid() && !$request->isPost())
С && скрипт умирал, только если не было ни сессии, ни POST. POST-запрос без sessid проходил, GET-запрос с sessid тоже. Теперь там ||, и нужны оба условия сразу. templateName во всех трёх читается только из POST и приводится к строке. В sale.personal.order.detail и sale.personal.order.list так же читается orderData, и из него выбрасываются нескалярные значения. sale.order.payment.change берёт данные заказа, как и раньше, из getPostList()->toArray() без этого фильтра.
Если кастомный шаблон этих компонентов шлёт запросы GET-ом или забывает sessid, после обновления он получит пустой ответ.
Платёж должен принадлежать платёжной системе из запроса
tools/sale_ps_ajax.php и tools/sale/applepay_gateway.php раньше вызывали initiatePay() для платежа по PAYMENT_ID и сервиса по PAYSYSTEM_ID, не проверяя, связаны ли они. Теперь запрос обрабатывается, только если заказ найден, платёж в нём есть и $payment->getPaymentSystemId() совпадает с PAYSYSTEM_ID из запроса. Во всех остальных случаях первый скрипт вернёт SALE_PS_AJAX_PARAMS_ERROR, второй STATUS_FAIL.
Фронтенд, который передаёт в sale_ps_ajax.php одну платёжную систему для платежа, заведённого на другую, теперь получит ошибку.
Импорт местоположений пускают только во временную папку
Функции saleLocationLoadFile() и saleLocationImport() из general/location_import.php раньше брали TMP_PATH, DLZIPFILE и адрес сервера из параметров как есть. Теперь:
TMP_PATHпроходит через новыйImport\TempPath::resolve()и должен быть абсолютным путём внутри временного каталога Битрикса (CTempFile::GetAbsoluteRoot()), причём проверка идёт и поrealpath(), так что симлинк наружу не поможет;DLZIPFILEобязан быть ровно'zip_ussr.csv';- скачивание идёт через новый
Import\Downloader, который пропускает толькоhttpиhttpsбез управляющих символов и запрещает запросы на приватные IP (setPrivateIp(false)).
Неверный TMP_PATH или DLZIPFILE функции отклоняют сразу с ошибкой SL_IMPORT_ERROR_FILES. Если адрес не пропустил Downloader, download() вернёт null, и на шаге скачивания выйдет ошибка загрузки SL_LOADER_FILE_ERROR. Скрипты мастера sale.locations (scripts/loader.php, scripts/import.php) теперь требуют POST и check_bitrix_sessid(), а его import.js переключили с CPHttpRequest.Send на CPHttpRequest.Post.
Своим скриптам, которые передают TMP_PATH за пределами временного каталога, например /home/bitrix/geo/, придётся перенести папку внутрь него. По коду main в эталонной установке это папка из опции upload_dir (по умолчанию upload) плюс /tmp в корне сайта, если не задана константа BX_TEMPORARY_FILES_DIRECTORY.
Пробейте тестовый чек
В кассовом модуле поменялся выбор ставки НДС для позиции чека. Cashbox\Check::getProductVatId() раньше брал НДС товара из каталога и только если его не было, искал ставку по BasketItem::getVatRate(), причём лишь при ставке больше нуля. Теперь порядок обратный. Сначала ищется ставка из корзины, включая нулевую, и только потом берётся НДС товара из каталога.
AbstractCheck::getVatIdByVatRate() сравнивает ставки как строки с двумя знаками после точки (number_format(..., 2, '.', '')) вместо (int), пропускает ставки с RATE = null и нечисловые значения. Для товаров, у которых ставка в корзине расходится с каталогом, в чек уйдёт другой идентификатор ставки.
Цена строки из формы товаров считается заново
Helpers\Order\Builder\Converter\CatalogJSProductForm превращает строки JS-формы товаров в позиции корзины. Приватный consistentFields() заменили на resolvePriceInBaseCoords(). PRICE теперь берётся из price при taxIncluded = 'Y' и из priceExclusive при 'N', раньше всегда из priceExclusive ?? price. DISCOUNT_PRICE считается как PriceMaths::roundPrecision(basePrice - price), а присланные формой discount и discountRate больше не читаются. Соберите на тестовом стенде заказ из формы в режиме «НДС сверху» и сверьте цену и скидку позиции.
Новое: TempPath и Downloader
Оба класса появились ради импорта местоположений, но они публичные и пригодятся в своём импорте, если нужно скачать файл и положить его во временную папку:
<?php declare(strict_types=1);
use Bitrix\Main\Loader;
use Bitrix\Main\Web\HttpClient;
use Bitrix\Sale\Location\Import\Downloader;
use Bitrix\Sale\Location\Import\TempPath;
Loader::requireModule('sale');
$dir = TempPath::resolve(null, 'vendor_geo');
// для null каталог только вычисляется, создать его нужно самим
if ($dir === null || !CheckDirPath($dir)) {
throw new \RuntimeException('Не удалось подготовить временную папку');
}
$csv = (new Downloader(new HttpClient()))->download(
server: 'https://geo.example.com',
port: null,
path: '/export/',
fileName: 'cities.csv',
method: 'GET',
);
if ($csv !== null) {
file_put_contents($dir . 'cities.csv', $csv);
}
С null вместо пути TempPath::resolve() отдаёт путь CTempFile::GetDirectoryName(12, $defaultContext) со слешем в конце, но саму папку не создаёт, поэтому в примере стоит CheckDirPath(). Переданный путь метод возвращает, только если тот лежит внутри временного каталога, и заодно создаёт папку. Иначе будет null. Downloader::download() склеивает адрес из сервера, порта, пути и имени файла и возвращает null, если адрес не прошёл проверку или ответ пришёл не строкой.
БД и установщик
Файлы install/db/{mysql,pgsql}/install.sql и uninstall.sql удалены, InstallDB() и UnInstallDB() вызывают унаследованные от CModule installMigrations() и uninstallMigrations(). Таблицы описаны в install/migrations/tables.php, агенты в agents.php (семь штук, от Product2ProductTable::deleteOldProducts(10) до агентов аналитики), события в events.php. В migration_config.json в качестве defaultTableName указана b_sale_auxiliary. Кроме install/components, install/js и install/tools, в маппинг каталогов входят install/gadgets, install/images и install/wizards.
Из обработчиков событий 77 описаны декларативно через register() и registerCompatible(), а ещё 24 старых вызова RegisterModuleDependences() остались как есть и обёрнуты в проверку $migration->context()->getDatabaseUpdateMode(). При ModuleInstall они регистрируются, при ModuleUninstall снимаются. Среди них main:OnBeforeProlog с подключением файла /modules/sale/affiliate.php. В сумме выходит 101 обработчик, столько же, сколько было в старом InstallDB().
tables.php описывает те же 146 таблиц, что и старый MySQL-скрипт, кроме шести колонок. SUM, PS_SUM, PRICE_COD в b_sale_order_payment и PRICE_DELIVERY, DISCOUNT_PRICE, BASE_PRICE_DELIVERY в b_sale_order_delivery в install.sql были decimal(18,4), в миграции стали decimal(26,8). В ORM эти поля PaymentTable и ShipmentTable получили 'scale' => 8. ALTER для существующих баз в диффе нет, так что после обновления стоит посмотреть, какая точность у колонок на вашей базе.
Ещё одно расхождение касается PostgreSQL. В старом pgsql/install.sql у TIMESTAMP_X девяти таблиц (b_sale_affiliate, b_sale_user_account, b_sale_recurring и других) стоял DEFAULT CURRENT_TIMESTAMP, в миграции его нет.
Мелочи и находки
- Ни одна правка в диффе не помечена как исправление уязвимости.
- Несколько защитных правок пришлись на админку. Кнопка создания стандартных отчётов в
admin/report.phpполучила проверкуsessid. Сортировка вadmin/exchange_log.phpпринимает только поля из заголовков таблицы и направленияASC/DESC. Создание Z-отчёта кассы вadmin/order_ajax.phpтребует права на модуль не нижеW. Названия политик eBay вebay_policy.phpиebay_wizard.phpтеперь экранируются. admin/print.phpэкранирует все строковые значения$arOrderPropsдо подключения шаблона печатной формы. Если ваш шаблон экранирует свойства заказа сам, после обновления получится двойное экранирование. Название товара вinvoice.phpиinvoice_en.phpтоже выводится черезhtmlspecialcharsbx().- Добавление платёжной системы через
Controller\Action\PaySystem\AddPaySystemActionпроверяетACTION_FILE. Это должна быть папка обработчика или точный, с учётом регистра, код REST-обработчика, сегменты.,..и пустые запрещены. Для отказа завели кодADD_PAY_SYSTEM_ACTION_ACTION_FILE_INVALID = 202650000015. - Из sale убрали
CHTTP::urlAddParams(),urlDeleteParams(),URN2URI(), созданиеnew CHTTP()иQueryGetData(). ВызовыCHTTP::SetStatus()в модуле остались. Старые обработчики доставки DHL USA, CPCR и EMS и проверка HTTPS вadmin/ymarket.phpперешли наHttpClient. У DHL USA и CPCR протокол теперь выбирается по порту из констант: 80 даётhttp, любой другойhttps. Helpers\Rest\Httpсоздаёт HTTP-клиент черезRest\Public\Provider\Application\HandlerHttpFactory, если модульrestустановлен и класс существует.TargetSaleMailConnector::onConnectorList()теперь возвращает пустой массив, и коннектор больше не предлагается модулю sender.- В
sale.personal.sectionу отменённого заказа ссылка на список заказов теряетfilter_history=Y. Раньшеshow_canceled=Yдобавлялся к уже изменённому пути, теперь к исходному. - В удалённом
install1.sqlлежали таблицыb_catalog_currency*с курсом доллара 30.2979. Ссылок на файл в диффе установщика нет. VERSION_DATEу 26.500.0 указан 15 июля 2026, раньше, чем у предыдущей 26.450.0 (11 сентября).- Deprecated в релизе нет.
Что делать
- Проверьте кастомные шаблоны
sale.order.payment.change,sale.personal.order.detailиsale.personal.order.list: запросы к ихajax.phpдолжны идти POST-ом и сsessid. - Если вызываете
saleLocationLoadFile()илиsaleLocationImport()сами, проверьте, чтоTMP_PATHлежит внутри временного каталога Битрикса, или не передавайте его вовсе. - Пробейте тестовый чек по товару, у которого ставка НДС в корзине отличается от ставки в каталоге.
- Посмотрите тип колонок
b_sale_order_payment.SUMиb_sale_order_delivery.PRICE_DELIVERYна своей базе после обновления. - Если печатная форма заказа своя, откройте её и проверьте, нет ли в свойствах заказа
&quot;.