
Хранимый XSS в J2Commerce Guest Checkout через обход cookie-фильтра
J2Commerce (com_j2store) ≤ 4.1.5 — неаутентифицированный злоумышленник сохраняет XSS-пейлоад, который автоматически выполняется в браузере администратора при загрузке страницы
J2Commerce 4.1.5 подвержен хранимому межсайтовому скриптингу (XSS) через поля платёжного адреса при гостевом оформлении заказа. Неаутентифицированный злоумышленник использует обход фильтра в Input::getArray() Joomla в сочетании с variables_order=EGPCS в PHP (Cookie переопределяет POST в $_REQUEST), чтобы сохранить неочищенный HTML в таких полях, как billing_first_name. Эти поля выводятся напрямую в панели управления заказами администратора без htmlspecialchars(), из-за чего пейлоад выполняется в браузере администратора.
XSS-пейлоад срабатывает автоматически при загрузке страницы, когда администратор переходит к списку заказов — клик по отдельному заказу не требуется. Цепочка из одного набора HTTP-запросов (добавление в корзину → отправка оформления заказа с cookie-обходом → размещение заказа) навсегда сохраняет пейлоад, который будет выполняться в браузере каждого администратора, пока заказ не будет удалён или уязвимость не исправлена.
Атака не требует аутентификации злоумышленника. Гостевое оформление заказа — стандартная, часто включённая функция интернет-магазинов, создающая у администраторов экономический стимул просматривать новые заказы, — что делает эксплуатацию тривиальной для применения в реальных атаках.
| КОМПОНЕНТ | УЯЗВИМЫЕ ВЕРСИИ | ПРОТЕСТИРОВАНО НА | ИСПРАВЛЕНО В |
|---|---|---|---|
| J2Commerce (com_j2store) | 1.0.0 – 4.1.5 | 4.1.5 на Joomla 5.4.7 + MySQL 8.0 | 3.3.21 / 4.0.21 / 4.1.6 |
Тип: межсайтовый скриптинг — хранимый (CWE-79)
Требуется аутентификация: нет — неаутентифицированный доступ (гостевое оформление)
Основной сток: administrator/components/com_j2store/views/orders/tmpl/default_items.php:73
Путь записи: components/com_j2store/controllers/checkouts.php:535
Уязвимость складывается из двух усиливающих друг друга слабостей: обхода входного фильтра на пути записи и отсутствия кодирования вывода на пути чтения.
1. Обход входного фильтра — неправильное использование Input::getArray() в Joomla
Контроллер гостевого оформления заказа J2Commerce читает поля адреса с помощью $app->input->getArray($_POST). Реализация Joomla перебирает массив $_POST и использует каждое значение как тип фильтра (а не как данные), при этом фактическое значение читается из $_REQUEST:
COMPONENTS/COM_J2STORE/CONTROLLERS/CHECKOUTS.PHP:535 — ПУТЬ ЗАПИСИ
$data = $app->input->getArray($_POST);
LIBRARIES/VENDOR/JOOMLA/INPUT/SRC/INPUT.PHP — МЕТОД GETARRAY() (СТРОКА 187)
public function getArray(array $vars = [], $datasource = null)
{
foreach ($vars as $k => $v) {
$results[$k] = $this->get($k, null, $v); // $k = field name, $v = POST value used as filter TYPE
}
}
LIBRARIES/VENDOR/JOOMLA/INPUT/SRC/INPUT.PHP — ИСТОЧНИК ДАННЫХ (СТРОКА 97)
$this->data = $source ?? $_REQUEST; // Reads from $_REQUEST, not $_POST
2. PHP variables_order — Cookie переопределяет POST в $_REQUEST
Суперглобальный массив $_REQUEST в PHP формируется объединением $_GET, $_POST и $_COOKIE. Когда variables_order=EGPCS (значение по умолчанию, встроенное в большинство окружений PHP), Cookie (C) указаны после POST (P), поэтому при конфликте ключей побеждает Cookie.
Отправка first_name=RAW в теле POST заставляет InputFilter::clean() в Joomla применить тип фильтра 'Raw' (no-op) к значению cookie first_name=<svg...>, которое побеждает в $_REQUEST.
LIBRARIES/VENDOR/JOOMLA/FILTER/SRC/INPUTFILTER.PHP — МЕТОД CLEAN() (СТРОКА 215)
$type = ucfirst(strtolower($type)); // 'RAW' → 'Raw'
if ($type === 'Raw') {
return $source; // ← no sanitization — returns cookie value unchanged
}
Итог: тело POST first_name=RAW устанавливает фильтр в режим no-op. Cookie first_name=<svg onload="alert(document.domain)"> побеждает в $_REQUEST. Joomla возвращает значение cookie без фильтрации. J2Commerce сохраняет его как есть в j2store_orderinfos.billing_first_name.
3. Отсутствие кодирования вывода — стоки в шаблонах администратора
ADMINISTRATOR/COMPONENTS/COM_J2STORE/VIEWS/ORDERS/TMPL/DEFAULT_ITEMS.PHP:73 — ОСНОВНОЙ СТОК (срабатывает при загрузке страницы списка)
// Vulnerable — no htmlspecialchars():
<span class="me-1"><?php echo $row->billing_first_name .' '.$row->billing_last_name; ?></span>
ADMINISTRATOR/COMPONENTS/COM_J2STORE/VIEWS/ORDER/TMPL/FORM_CUSTOMER.PHP:56 — ВТОРИЧНЫЙ СТОК
// Vulnerable — no htmlspecialchars():
<?php echo '<strong>'.$this->orderinfo->billing_first_name." ".$this->orderinfo->billing_last_name."</strong>"; ?>
<?php echo $this->orderinfo->billing_address_1;?>
<?php echo $this->orderinfo->billing_city;?>
<?php echo $this->orderinfo->billing_phone_1; ?>
Этот обход работает во всех окружениях PHP, кроме Debian/Ubuntu (где явно задано request_order = "GP", исключающее cookies из $_REQUEST). Все остальные популярные хостинговые окружения — общий хостинг cPanel/Plesk, CentOS/RHEL, XAMPP/WAMP/MAMP, Windows IIS — используют значение по умолчанию EGPCS, поэтому переопределение через Cookie активно сразу из коробки, без каких-либо изменений конфигурации.
Откройте список заказов J2Commerce в панели администратора от имени администратора-жертвы. Это подтверждает, что администратор активно использует панель и встретит пейлоад при следующем посещении.

Будучи неаутентифицированным злоумышленником, отправьте GET-запрос на главную страницу витрины J2Commerce, чтобы установить сессию и извлечь CSRF-токен, встроенный в JSON-параметры страницы. Этот токен требуется для последующих POST-запросов.

Добавьте товар в корзину злоумышленника. Корзина должна быть непустой, чтобы конечная точка гостевого оформления заказа приняла отправку адреса.
POST /index.php?option=com_j2store&view=carts&task=addItem&ajax=1 HTTP/1.1
Host: target.example.com
Content-Type: application/x-www-form-urlencoded
product_id=&j2store_variant_id=&quantity=1&<csrf_token>=1

Отправьте форму адреса гостевого оформления с двумя конфликтующими значениями first_name: