security: KVE-2026-2345 장바구니 옵션 검증으로 상품대금 누락 차단

공통·기본 테마 AJAX와 일반 장바구니 요청에 동일한 상품·옵션 검증을 적용한다. 주문 준비 및 PC·모바일 주문 제출에서도 기존 행의 종류·가격·구성을 확인한다.

공통 규칙 46개와 격리 MySQL의 MyISAM·InnoDB HTTP/DB 검증 126개를 통과했다. 주문 검증은 PG·주문 저장 이전 경계까지이며 전체 결제 및 동시성 검증은 별도로 필요하다.
This commit is contained in:
whitedot
2026-09-10 05:17:59 +00:00
parent 77e0cda39b
commit 6d4e75e1c9
11 changed files with 507 additions and 98 deletions
+13 -33
View File
@@ -78,35 +78,20 @@ switch ($action) {
if ($count < 1)
die(json_encode(array('error' => '장바구니에 담을 상품을 선택하여 주십시오.')));
$cart_validation = shop_validate_cart_request($_POST);
if ($cart_validation['error'] !== '')
die(json_encode(array('error' => $cart_validation['error'])));
$ct_count = 0;
for($i=0; $i<$count; $i++) {
$it_id = isset($_POST['it_id'][$i]) ? safe_replace_regex($_POST['it_id'][$i], 'it_id') : '';
$opt_count = (isset($_POST['io_id'][$it_id]) && is_array($_POST['io_id'][$it_id])) ? count($_POST['io_id'][$it_id]) : 0;
// 상품정보
$it = get_shop_item($it_id, false);
$it = $cart_validation['products'][$it_id]['item'];
if(!$it['it_id'])
die(json_encode(array('error' => '상품정보가 존재하지 않습니다.')));
// 옵션정보를 얻어서 배열에 저장
$opt_list = array();
$sql = " select * from {$g5['g5_shop_item_option_table']} where it_id = '$it_id' order by io_no asc ";
$result = sql_query($sql);
$lst_count = 0;
for($k=0; $row=sql_fetch_array($result); $k++) {
$opt_list[$row['io_type']][$row['io_id']]['id'] = $row['io_id'];
$opt_list[$row['io_type']][$row['io_id']]['use'] = $row['io_use'];
$opt_list[$row['io_type']][$row['io_id']]['price'] = $row['io_price'];
$opt_list[$row['io_type']][$row['io_id']]['stock'] = $row['io_stock_qty'];
// 선택옵션 개수
if(!$row['io_type'])
$lst_count++;
}
if($lst_count > 0 && !trim($_POST['io_id'][$it_id][$i]) && $_POST['io_type'][$it_id][$i] == 0)
die(json_encode(array('error' => '상품의 선택옵션을 선택해 주십시오.')));
for($k=0; $k<$opt_count; $k++) {
$post_ct_qty = isset($_POST['ct_qty'][$it_id][$k]) ? (int) $_POST['ct_qty'][$it_id][$k] : 0;
if ($post_ct_qty < 1)
@@ -155,25 +140,17 @@ switch ($action) {
// 장바구니에 Insert
$comma = '';
$ct_count = 0;
$sql = " INSERT INTO {$g5['g5_shop_cart_table']}
( od_id, mb_id, it_id, it_name, it_sc_type, it_sc_method, it_sc_price, it_sc_minimum, it_sc_qty, ct_status, ct_price, ct_point, ct_point_use, ct_stock_use, ct_option, ct_qty, ct_notax, io_id, io_type, io_price, ct_time, ct_ip, ct_send_cost, ct_direct, ct_select, ct_select_time )
VALUES ";
for($k=0; $k<$opt_count; $k++) {
$io_id = isset($_POST['io_id'][$it_id][$k]) ? preg_replace(G5_OPTION_ID_FILTER, '', $_POST['io_id'][$it_id][$k]) : '';
$io_type = isset($_POST['io_type'][$it_id][$k]) ? preg_replace('#[^01]#', '', $_POST['io_type'][$it_id][$k]) : '';
$io_id = $cart_validation['products'][$it_id]['rows'][$k]['io_id'];
$io_type = $cart_validation['products'][$it_id]['rows'][$k]['io_type'];
$io_value = isset($_POST['io_value'][$it_id][$k]) ? $_POST['io_value'][$it_id][$k] : '';
// 선택옵션정보가 존재하는데 선택된 옵션이 없으면 건너뜀
if($lst_count && $io_id == '')
continue;
$opt_list_type_id_use = isset($opt_list[$io_type][$io_id]['use']) ? $opt_list[$io_type][$io_id]['use'] : '';
// 구매할 수 없는 옵션은 건너뜀
if($io_id && ! $opt_list_type_id_use)
continue;
$io_price = isset($opt_list[$io_type][$io_id]['price']) ? $opt_list[$io_type][$io_id]['price'] : 0;
$io_price = $cart_validation['products'][$it_id]['rows'][$k]['io_price'];
$ct_qty = isset($_POST['ct_qty'][$it_id][$k]) ? (int) $_POST['ct_qty'][$it_id][$k] : 0;
// 구매가격이 음수인지 체크
@@ -186,14 +163,17 @@ switch ($action) {
}
// 동일옵션의 상품이 있으면 수량 더함
$sql2 = " select ct_id, io_type, ct_qty
$sql2 = " select ct_id, io_type, ct_qty, ct_price, io_price
from {$g5['g5_shop_cart_table']}
where od_id = '$tmp_cart_id'
and it_id = '$it_id'
and io_id = '$io_id'
and io_type = '$io_type'
and ct_status = '쇼핑' ";
$row2 = sql_fetch($sql2);
if(isset($row2['ct_id']) && $row2['ct_id']) {
if ((int) $row2['ct_price'] !== (int) $it['it_price'] || (int) $row2['io_price'] !== $io_price)
die(json_encode(array('error' => '기존 장바구니의 상품 정보가 변경되었습니다. 삭제한 뒤 다시 담아 주십시오.')));
// 재고체크
$tmp_ct_qty = $row2['ct_qty'];
if(!$io_id)