Коротко
Content-Type: multipart/form-data.application/x-www-form-urlencoded. Изменили 12 файлов шаблона, ядро и модули не тронули. Уязвимость этот обход не открывает: такие запросы не попадают в разбор multipart, где находится ошибка PHP.PHP2Shell — общее название двух уязвимостей в ядре PHP. Обе получили 9,2 из 10 по CVSS — международной шкале опасности уязвимостей — и позволяют выполнить код на сервере без пароля. Обе связаны с multipart/form-data — форматом, в котором браузер отправляет форму с файлами. Обычные формы без файлов уходят в формате application/x-www-form-urlencoded, то есть строкой «поле=значение».
multipart/form-data и ему не хватает памяти, очистка работает с неинициализированными данными.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 |
|---|---|---|
| Открывал карточку товара в мобильной версии | 865 | 226 |
| Нажимал «В корзину» | 82 | 12 |
| Регистрировался | 23 | 15 |
| Оформлял заказ | 10 | 2 |
Часть IP — наши собственные проверки, отдельно мы их не вычитали.
Кроме этого, не работали формы «Заказать звонок» и «Написать сообщение», а также отзывы о товарах. Владелец магазина видел одно: кнопки не реагируют. Сервер возвращал HTML-страницу 403, а скрипты Аспро ждали 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 нужен, чтобы передать файл. Но в Аспро и в стандартных компонентах Битрикса этот формат используют и там, где файлов нет:
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: в любой форме может появиться вопрос с файлом.Файлы здесь отправляют только регистрация с фото и отзыв с фотографиями. Под правило хостинга попало почти всё остальное: корзина, заказ, обратные звонки.
Принцип один: если в форме нет файлов, отправлять её как application/x-www-form-urlencoded. Этот формат PHP разбирает другим кодом, не тем, где находится ошибка, поэтому обход не ослабляет защиту хостинга. Там, где файлы действительно есть, multipart остаётся.
FormData — объект JavaScript, в который скрипт собирает поля формы; браузер всегда отправляет его как multipart. Поэтому в коде мы переводим FormData в URLSearchParams — строку параметров urlencoded.
Мы изменили 12 файлов в шаблонах сайта. Каждый перед заменой сверили с серверной версией и сохранили в резервную копию. Ядро Битрикса и модули Аспро не трогали. Каждое изменение помечено комментарием TEMP 22.10.2026, чтобы их было легко найти и откатить.
В настройках запроса FormData заменили на строку параметров, а preparePost включили, чтобы BX.ajax отправил правильный заголовок:
let config = {
url: this.requestUrl,
data: new URLSearchParams(data).toString(),
method: 'POST',
dataType: 'json',
preparePost: true,
// остальные параметры без изменений
}; Ту же правку мы внесли в сжатый item-action.min.js, чтобы версии не расходились.
fetch(`${arAsproOptions["SITE_TEMPLATE_PATH_MOBILE"]}/ajax/itemInfo.php`, {
method: "POST",
body: new URLSearchParams(data),
}) 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. Правка сжатой версии ничего бы не дала. Перед изменением проверяйте, какой файл реально загружает браузер, во вкладке «Сеть» инструментов разработчика.
Атрибут 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;?>> В шаблонах 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"]);?> Формат выбирается в момент отправки: если покупатель приложил фото, остаётся 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 и 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 года, поэтому переходить на него сейчас нет смысла.
multipart/form-data до обновления PHP. Проверьте access-лог: если POST-запросы получают 403, а в журнале ошибок Apache их нет, запрос отклоняет веб-сервер хостинга ещё до сайта.application/x-www-form-urlencoded PHP разбирает отдельным кодом, а все известные атаки PHP2Shell используют multipart. Обход только возвращает работу формам без файлов. Защиту хостинга он не снимает.