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)
+15 -31
View File
@@ -146,6 +146,10 @@ else // 장바구니에 담기
if ($count < 1)
alert('장바구니에 담을 상품을 선택하여 주십시오.');
$cart_validation = shop_validate_cart_request($_POST, $act === 'multi');
if ($cart_validation['error'] !== '')
alert($cart_validation['error']);
$ct_count = 0;
$post_chk_it_id = (isset($_POST['chk_it_id']) && is_array($_POST['chk_it_id'])) ? $_POST['chk_it_id'] : array();
$post_io_ids = (isset($_POST['io_id']) && is_array($_POST['io_id'])) ? $_POST['io_id'] : array();
@@ -184,7 +188,7 @@ else // 장바구니에 담기
}
// 상품정보
$it = get_shop_item($it_id, false);
$it = $cart_validation['products'][$it_id]['item'];
if(!$it['it_id'])
alert('상품정보가 존재하지 않습니다.');
@@ -219,30 +223,14 @@ else // 장바구니에 담기
}
}
// 옵션정보를 얻어서 배열에 저장
$opt_list = array();
$sql = " select * from {$g5['g5_shop_item_option_table']} where it_id = '$it_id' and io_use = 1 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($sw_direct) {
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] : '';
$sql = " select SUM(ct_qty) as cnt from {$g5['g5_shop_cart_table']}
@@ -287,24 +275,17 @@ else // 장바구니에 담기
// 장바구니에 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;
// 구매할 수 없는 옵션은 건너뜀
if($io_id && !$opt_list[$io_type][$io_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;
// 구매가격이 음수인지 체크
@@ -317,14 +298,17 @@ else // 장바구니에 담기
}
// 동일옵션의 상품이 있으면 수량 더함
$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)
alert('기존 장바구니의 상품 정보가 변경되었습니다. 삭제한 뒤 다시 담아 주십시오.');
// 재고체크
$tmp_ct_qty = $row2['ct_qty'];
if(!$io_id)
+6
View File
@@ -24,6 +24,12 @@ else {
if (get_cart_count($tmp_cart_id) == 0)
alert('장바구니가 비어 있습니다.', G5_SHOP_URL.'/cart.php');
// KVE-2026-2345: 과거 장바구니의 옵션 종류와 가격도 주문 전에 다시 검증한다.
$cart_validation_error = shop_validate_order_cart($tmp_cart_id);
if ($cart_validation_error !== '') {
alert($cart_validation_error, G5_SHOP_URL.'/cart.php');
}
if (function_exists('before_check_cart_price')) {
if(! before_check_cart_price($tmp_cart_id) ) alert('장바구니 금액에 변동사항이 있습니다.\n장바구니를 다시 확인해 주세요.', G5_SHOP_URL.'/cart.php');
}
+6
View File
@@ -47,6 +47,12 @@ if (get_cart_count($tmp_cart_id) == 0) { // 장바구니에 담기
alert('장바구니가 비어 있습니다.\\n\\n이미 주문하셨거나 장바구니에 담긴 상품이 없는 경우입니다.', G5_SHOP_URL.'/cart.php');
}
// KVE-2026-2345: 과거 장바구니의 옵션 종류와 가격도 주문 전에 다시 검증한다.
$cart_validation_error = shop_validate_order_cart($tmp_cart_id);
if ($cart_validation_error !== '') {
alert($cart_validation_error, G5_SHOP_URL.'/cart.php');
}
// 변수 초기화
$od_other_pay_type = '';