Вкладка «Сеть» в инструментах разработчика: запросы корзины и заказа на Аспро получают 403 от nginx из-за multipart/form-data

Уязвимость PHP2Shell: хостинг закрыл multipart — как мы решали проблему на Аспро и WordPress

ЕП
Евгений Панарин
программист 1С-Битрикс в 360i Digital. Работает с 1С-Битрикс и решениями Аспро больше 7 лет.
Опубликовано: 22.10.2026 · Обновлено: 22.10.2026

Коротко

  • 21 октября 2026 года вышли исправления критических уязвимостей PHP2Shell (CVE-2026-109618 и CVE-2026-109619). Чтобы закрыть атаку до обновления PHP, reg.ru на виртуальном хостинге начал отвечать ошибкой 403 на любой запрос с Content-Type: multipart/form-data.
  • В интернет-магазине косметики на 1С-Битрикс и Аспро: Лайтшоп перестали работать корзина, мобильная карточка товара, регистрация, оформление заказа, формы обратной связи и отзывы. Ошибок на экране не было: кнопки просто не реагировали.
  • За сутки блокировки сервер отклонил 980 запросов покупателей. Больше всего пострадала мобильная версия: 865 отказов с 226 IP-адресов.
  • Мы нашли причину по логам и перевели все формы без файлов на обычную отправку application/x-www-form-urlencoded. Изменили 12 файлов шаблона, ядро и модули не тронули. Уязвимость этот обход не открывает: такие запросы не попадают в разбор multipart, где находится ошибка PHP.

Что такое PHP2Shell и почему хостинги закрыли multipart?

PHP2Shell — общее название двух уязвимостей в ядре PHP. Обе получили 9,2 из 10 по CVSS — международной шкале опасности уязвимостей — и позволяют выполнить код на сервере без пароля. Обе связаны с multipart/form-data — форматом, в котором браузер отправляет форму с файлами. Обычные формы без файлов уходят в формате application/x-www-form-urlencoded, то есть строкой «поле=значение».

  • CVE-2026-109618. Когда PHP разбирает запрос multipart/form-data и ему не хватает памяти, очистка работает с неинициализированными данными.
  • CVE-2026-109619. После фатальной ошибки состояние процесса не сбрасывается, и следующий запрос обращается к уже освобождённой памяти. Одна из цепочек атаки использует отслеживание прогресса загрузки session.upload_progress, которое тоже работает через multipart.

Детали утекли 20 октября, за сутки до релиза. Исправления вышли 21 октября в версиях PHP 8.5.12, 8.4.27, 8.3.36 и 8.2.35. Для 7.4, 8.0 и 8.1 разработчики PHP выложили только патчи в исходном коде: готовых версий для них нет. Попытки атак фиксировали ещё до выхода исправлений.

По данным Sansec, все известные способы атаки идут через multipart/form-data. PHP разбирает такой запрос раньше, чем начинает работать код сайта, поэтому ни 1С-Битрикс, ни плагины, ни режим обслуживания не защищают. Быстрее всего атаку закрывает правило на веб-сервере перед PHP: запросы с multipart получают отказ. Sansec прямо предупреждает, что такое правило «ломает загрузку файлов и иногда оформление заказов».

Этим путём пошли хостинги. reg.ru ограничил multipart на виртуальном хостинге и снимает ограничение по мере перехода сайтов на исправленные версии PHP. Beget временно делал то же самое: у пользователей MODX из-за этого перестала сохраняться админка.

Решение хостинга понятное: сутки без форм обходятся дешевле, чем взломанные сайты клиентов. Плохо другое. Публичного уведомления от reg.ru мы не нашли, и владелец магазина узнал о блокировке от нас.

Что сломалось в магазине на Аспро: Лайтшоп?

Наш клиент — интернет-магазин натуральной косметики на 1С-Битрикс с решением Аспро: Лайтшоп. Отдельные шаблоны для компьютера и телефона, виртуальный хостинг reg.ru, PHP 8.0.

21 октября в 16:33 по Москве сервер впервые ответил 403 на запрос с multipart. До этого все запросы проходили. Вот сколько запросов покупателей сервер отклонил до 14:28 22 октября:

Что делал покупательОтклонено запросовУникальных IP
Открывал карточку товара в мобильной версии865226
Нажимал «В корзину»8212
Регистрировался2315
Оформлял заказ102

Часть IP — наши собственные проверки, отдельно мы их не вычитали.

Кроме этого, не работали формы «Заказать звонок» и «Написать сообщение», а также отзывы о товарах. Владелец магазина видел одно: кнопки не реагируют. Сервер возвращал HTML-страницу 403, а скрипты Аспро ждали JSON, не могли его разобрать и ничего не показывали.

Вкладка «Сеть» в инструментах разработчика: запросы корзины, мобильной карточки, регистрации и заказа получают 403 от nginx, Content-Type — multipart/form-data
Рисунок 1. Так выглядела блокировка в браузере: запросы с multipart получали HTML-страницу 403 от nginx вместо JSON. Воспроизведено по логам, адрес сайта скрыт.

Первая версия клиента — «закончилось место на хостинге». Она не подтвердилась: запись на диск работала.

Как мы нашли причину?

Сначала мы проверили логи. Отказы были в access-логе, но не в журнале ошибок Apache. Значит, запрос отклонял nginx хостинга ещё до того, как он доходил до сайта. Файл .htaccess не менялся с февраля.

Затем мы проверили гипотезу одним запросом: отправили пустой POST с заголовком multipart/form-data на произвольный адрес сайта и получили 403. Тот же запрос в формате application/x-www-form-urlencoded прошёл. Значит, сервер режет любой запрос с multipart, есть в нём файлы или нет, и на любом адресе.

Поддержка reg.ru подтвердила: «временно ограничили загрузку файлов через multipart/form-data», ограничения сохранятся «в течение ближайших суток». Другим клиентам на форумах называли срок «несколько дней» и упоминали проблемы со входом в админку.

«Если в магазине на Битриксе молча перестали работать корзина и формы, а хостинг виртуальный — первым делом посмотрите в access-логе ответы 403 на POST-запросы. Пять минут на проверку экономят день на поиск ошибки в коде». — Евгений Панарин, программист 1С-Битрикс

Почему формы без файлов отправлялись как multipart?

Multipart нужен, чтобы передать файл. Но в Аспро и в стандартных компонентах Битрикса этот формат используют и там, где файлов нет:

  • Корзина и сравнение. Скрипт item-action.js собирает данные в объект FormData, а браузер всегда отправляет FormData как multipart.
  • Мобильная карточка товара. itemInfo.js запрашивает состояние корзины через fetch с тем же FormData.
  • Регистрация. В шаблоне main.register у формы жёстко прописан enctype="multipart/form-data": в регистрации может быть поле для фото.
  • Оформление заказа. sale.order.ajax отправляет форму через BX.ajax.submitAjax, который всегда использует multipart.
  • Веб-формы. form.result.new выводит FORM_HEADER с multipart: в любой форме может появиться вопрос с файлом.
  • Отзывы. У формы отзыва multipart, потому что к отзыву можно приложить фото.

Файлы здесь отправляют только регистрация с фото и отзыв с фотографиями. Под правило хостинга попало почти всё остальное: корзина, заказ, обратные звонки.

Как мы вернули работу магазина, не дожидаясь хостинга?

Принцип один: если в форме нет файлов, отправлять её как application/x-www-form-urlencoded. Этот формат PHP разбирает другим кодом, не тем, где находится ошибка, поэтому обход не ослабляет защиту хостинга. Там, где файлы действительно есть, multipart остаётся.

Схема: запрос multipart блокируется nginx хостинга, тот же запрос в urlencoded проходит до PHP и Битрикса
Рисунок 2. До обхода запрос «В корзину» останавливался на nginx хостинга, после — доходит до Битрикса в формате urlencoded.

FormData — объект JavaScript, в который скрипт собирает поля формы; браузер всегда отправляет его как multipart. Поэтому в коде мы переводим FormData в URLSearchParams — строку параметров urlencoded.

Мы изменили 12 файлов в шаблонах сайта. Каждый перед заменой сверили с серверной версией и сохранили в резервную копию. Ядро Битрикса и модули Аспро не трогали. Каждое изменение помечено комментарием TEMP 22.10.2026, чтобы их было легко найти и откатить.

Корзина: item-action.js

В настройках запроса FormData заменили на строку параметров, а preparePost включили, чтобы BX.ajax отправил правильный заголовок:

let config = {
    url: this.requestUrl,
    data: new URLSearchParams(data).toString(),
    method: 'POST',
    dataType: 'json',
    preparePost: true,
    // остальные параметры без изменений
};

Ту же правку мы внесли в сжатый item-action.min.js, чтобы версии не расходились.

Мобильная карточка товара: itemInfo.js

fetch(`${arAsproOptions["SITE_TEMPLATE_PATH_MOBILE"]}/ajax/itemInfo.php`, {
    method: "POST",
    body: new URLSearchParams(data),
})

Оформление заказа: order_ajax.js

BX.ajax.submitAjax заменили на обычный BX.ajax. Поля формы собираются через FormData и переводятся в строку параметров:

BX.ajax({
    url: this.ajaxUrl,
    method: 'POST',
    dataType: 'json',
    preparePost: true,
    data: (function (form, extra) {
        var params = new URLSearchParams(new FormData(form));
        for (var key in extra) params.set(key, extra[key]);
        return params.toString();
    })(BX('bx-soa-order-form'), {
        via_ajax: 'Y',
        action: 'saveOrderAjax',
        sessid: BX.bitrix_sessid(),
        SITE_ID: this.siteId,
        signedParamsString: this.signedParamsString
    }),
    onsuccess: BX.proxy(this.saveOrderWithJson, this),
    onfailure: BX.proxy(this.handleNotRedirected, this)
});

Здесь легко ошибиться. В папке шаблона лежит order_ajax.min.js, но он старше исходника, а компонент подключает order_ajax.js. Правка сжатой версии ничего бы не дала. Перед изменением проверяйте, какой файл реально загружает браузер, во вкладке «Сеть» инструментов разработчика.

Регистрация: main.register

Атрибут enctype выводится, только если в форме есть поле-файл: фото, логотип или пользовательское поле типа «Файл».

<?$bRegFiles = array_intersect(['PERSONAL_PHOTO', 'WORK_LOGO'], (array)$arResult['SHOW_FIELDS'])
    || array_filter((array)$arResult['USER_PROPERTIES']['DATA'], fn($f) => ($f['USER_TYPE_ID'] ?? '') === 'file');?>
<form id="registraion-page-form" method="post" action="<?=POST_FORM_ACTION_URI?>" name="regform"<?if ($bRegFiles):?> enctype="multipart/form-data"<?endif;?>>

Веб-формы: form.result.new

В шаблонах popup и order проверяем, есть ли среди вопросов поля типа file или image. Если нет, убираем multipart из FORM_HEADER:

<?$bFormFiles = false;
foreach ((array)$arResult["QUESTIONS"] as $arFormQuestion) {
    foreach ((array)$arFormQuestion["STRUCTURE"] as $arFormAnswer) {
        if (in_array($arFormAnswer["FIELD_TYPE"] ?? "", ["file", "image"], true)) $bFormFiles = true;
    }
}
echo $bFormFiles
    ? $arResult["FORM_HEADER"]
    : preg_replace('/\s+enctype=(["\'])multipart\/form-data\1/i', "", $arResult["FORM_HEADER"]);?>

Отзывы: blog.post.comment.lite

Формат выбирается в момент отправки: если покупатель приложил фото, остаётся multipart, если нет — urlencoded.

obForm = BX('form_comment');
var bCommentFiles = Array.prototype.some.call(
    obForm.querySelectorAll('input[type=file]'),
    function (input) { return input.files && input.files.length; }
);
obForm.enctype = bCommentFiles ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

Все правки сделаны в обоих шаблонах — десктопном aspro-lite и мобильном aspro-lite-mobile.

Как мы проверили, не оформляя тестовых заказов?

Проверяли в браузере Chrome через Playwright на двух устройствах: компьютер и эмуляция iPhone 13. Сценарии: добавление в корзину, мобильная карточка товара, регистрация, оформление заказа, «Заказать звонок», «Написать сообщение», отзыв.

Тестовых заказов и заявок мы не создавали. Отправка форм перехватывалась в браузере: мы смотрели, в каком формате ушёл запрос, и обрывали его. Оформление заказа проверили запросом с пустыми обязательными полями. Сервер ответил 200 и вернул ошибки валидации, а значит, запрос прошёл мимо блокировки, но заказ не создался.

Во всех сценариях запросы уходили в формате urlencoded и получали ответ 200.

А если сайт на WordPress?

В тот же день с той же блокировкой к нам обратилась пекарня с сайтом на WordPress и Contact Form 7 на том же хостинге с PHP 7.4. Contact Form 7 версии 5.x отправляет заявку через fetch с FormData, то есть всегда как multipart, поэтому заявки с сайта перестали приходить.

Решение — один файл в wp-content/mu-plugins/. Плагины из этой папки WordPress загружает автоматически, их нельзя случайно отключить в админке. Он подменяет fetch только для запросов к contact-form-7/v1/ и только если к форме не приложены файлы. Ни плагин, ни тема не меняются, а откат — удалить файл.

<?php
add_action('wp_enqueue_scripts', function () {
    if (!wp_script_is('contact-form-7', 'registered')) {
        return;
    }
    $script = <<<'JS'
(function () {
    if (!window.fetch || !window.FormData || !window.URLSearchParams) return;
    var originalFetch = window.fetch;
    window.fetch = function (resource, init) {
        var url = typeof resource === 'string' ? resource : (resource && resource.url) || '';
        if (init && init.body instanceof FormData && url.indexOf('contact-form-7/v1/') !== -1) {
            var params = new URLSearchParams(), hasFiles = false;
            init.body.forEach(function (value, key) {
                if (value instanceof File) { if (value.size > 0) hasFiles = true; }
                else params.append(key, value);
            });
            if (!hasFiles) init = Object.assign({}, init, { body: params });
        }
        return originalFetch.call(this, resource, init);
    };
})();
JS;
    wp_add_inline_script('contact-form-7', $script, 'before');
}, 100);

После установки форма вернула ответ 200, а настоящая заявка с сайта снова дошла до владельца.

Чем закончилось и что делать дальше?

22 октября около 16:40 по Москве хостинг снял блокировку с магазина: настоящий multipart-запрос «В корзину» снова получил ответ 200. Сайт пекарни на PHP 7.4 на тот момент оставался под блокировкой.

Что получил магазинРезультат
Корзина, мобильная версия, регистрация, заказработают в день обращения, до снятия блокировки
Формы обратной связи и отзывыработают; фото в отзывах по-прежнему уходят как multipart
Безопасностьобход не открывает уязвимость: формы без файлов не проходят через разбор multipart
Откат12 файлов с пометкой TEMP 22.10.2026, оригиналы в резервной копии и в репозитории

Временные правки безвредны, но после снятия блокировки их стоит откатить: следующие обновления Аспро: Лайтшоп могут затронуть эти файлы.

Обход не заменяет обновление PHP. Магазин работает на PHP 8.0, а для 8.0 нет официальных исправленных версий. Сам Аспро в новости от 22 октября называет такие сайты незащищёнными и советует повышать версию PHP. Следующий шаг для клиента — переход на PHP 8.4.27 с проверкой собственных доработок, в том числе подарков в корзине. Поддержка PHP 8.2 заканчивается 31 декабря 2026 года, поэтому переходить на него сейчас нет смысла.

Частые вопросы

Почему после PHP2Shell на сайте ошибка 403?
Скорее всего, хостинг временно блокирует запросы multipart/form-data до обновления PHP. Проверьте access-лог: если POST-запросы получают 403, а в журнале ошибок Apache их нет, запрос отклоняет веб-сервер хостинга ещё до сайта.
Поможет ли обновление 1С-Битрикс или Аспро?
Нет. Уязвимость находится в самом PHP. 1С-Битрикс прямо пишет, что обновление через SiteUpdate её не закрывает. Нужна исправленная версия PHP: 8.5.12, 8.4.27, 8.3.36 или 8.2.35.
Безопасно ли отправлять формы без multipart?
Да. Формат application/x-www-form-urlencoded PHP разбирает отдельным кодом, а все известные атаки PHP2Shell используют multipart. Обход только возвращает работу формам без файлов. Защиту хостинга он не снимает.
Что будет с загрузкой файлов?
Пока хостинг блокирует multipart, загрузить файл через сайт не получится ни при каком обходе. Наши правки оставляют multipart там, где файлы действительно есть, чтобы после снятия блокировки загрузка заработала без изменений.
Когда хостинг снимет блокировку?
reg.ru снимает ограничение после перехода сайта на исправленную версию PHP. Клиентам называли сроки от «ближайших суток» до «нескольких дней». Быстрее всего — самому переключить сайт на PHP 8.4 в панели хостинга, предварительно проверив совместимость.
У меня PHP 7.4 или 8.0. Что делать?
Официальных исправленных версий для 7.4, 8.0 и 8.1 нет, есть только патчи в исходном коде для самостоятельной сборки. На виртуальном хостинге это решает провайдер, поэтому надёжнее перейти на PHP 8.3 или 8.4. Кастомные доработки проверьте до переключения.
Made on
Tilda