Files
Gnuboard5/lib/shop.cartvalidate.lib.php
T
whitedot 6d4e75e1c9 security: KVE-2026-2345 장바구니 옵션 검증으로 상품대금 누락 차단
공통·기본 테마 AJAX와 일반 장바구니 요청에 동일한 상품·옵션 검증을 적용한다. 주문 준비 및 PC·모바일 주문 제출에서도 기존 행의 종류·가격·구성을 확인한다.

공통 규칙 46개와 격리 MySQL의 MyISAM·InnoDB HTTP/DB 검증 126개를 통과했다. 주문 검증은 PG·주문 저장 이전 경계까지이며 전체 결제 및 동시성 검증은 별도로 필요하다.
2026-09-10 05:17:59 +00:00

160 lines
7.1 KiB
PHP

<?php
if (!defined('_GNUBOARD_')) exit;
// KVE-2026-2345: 요청의 옵션 종류는 가격 계산의 근거가 될 수 없다.
// DB 조회와 분리하여 신규 요청과 기존 장바구니에 같은 규칙을 적용한다.
function shop_validate_cart_rows($item, $options, $rows, $check_price = false)
{
$error = '상품 또는 옵션 정보가 올바르지 않습니다. 장바구니에서 삭제한 뒤 다시 담아 주십시오.';
if (empty($item['it_id']) || empty($item['it_use']) || !empty($item['it_tel_inq']) || !$rows)
return array('error' => $error);
$option_map = array();
$has_selection = false;
foreach ($options as $option) {
if ((string) $option['it_id'] !== (string) $item['it_id'])
return array('error' => $error);
if ((string) $option['io_type'] === '0')
$has_selection = true;
$key = $option['io_type'].':'.$option['io_id'];
if (isset($option_map[$key]))
return array('error' => $error);
$option_map[$key] = $option;
}
$validated = array();
$quantities = array();
$base_qty = 0;
$total = 0;
foreach ($rows as $row) {
if (!isset($row['io_id'], $row['io_type'], $row['ct_qty']) ||
!is_string($row['io_id']) ||
!(is_string($row['io_type']) || is_int($row['io_type'])) ||
!in_array((string) $row['io_type'], array('0', '1'), true) ||
!(is_string($row['ct_qty']) || is_int($row['ct_qty'])) ||
!preg_match('/^[1-9][0-9]*$/D', (string) $row['ct_qty']) ||
(float) $row['ct_qty'] > 2147483647)
return array('error' => $error);
$type = (int) $row['io_type'];
if (!$validated && $type !== 0)
return array('error' => $error);
$id = $row['io_id'];
$key = $type.':'.$id;
if ($id === '') {
if ($type !== 0 || $has_selection)
return array('error' => $error);
$price = 0;
$stock = (int) $item['it_stock_qty'];
} else {
if (!isset($option_map[$key]) || empty($option_map[$key]['io_use']))
return array('error' => $error);
$option = $option_map[$key];
$type = (int) $option['io_type'];
$price = (int) $option['io_price'];
$stock = (int) $option['io_stock_qty'];
}
$qty = (int) $row['ct_qty'];
$quantities[$key] = isset($quantities[$key]) ? $quantities[$key] + $qty : $qty;
if ($quantities[$key] > $stock)
return array('error' => '상품 또는 옵션의 재고수량이 부족합니다.');
$unit_price = $type === 1 ? $price : (int) $item['it_price'] + $price;
if ($unit_price < 0)
return array('error' => $error);
if ($check_price && (!isset($row['ct_price'], $row['io_price']) ||
(int) $row['ct_price'] !== (int) $item['it_price'] || (int) $row['io_price'] !== $price))
return array('error' => '상품 또는 옵션 금액이 변경되었습니다. 장바구니를 다시 확인해 주십시오.');
if ($type === 0)
$base_qty += $qty;
$row['io_type'] = $type;
$row['io_price'] = $price;
$row['ct_price'] = (int) $item['it_price'];
$row['ct_qty'] = $qty;
$validated[] = $row;
$total += $unit_price * $qty;
}
if (!$base_qty || (!empty($item['it_buy_min_qty']) && $base_qty < $item['it_buy_min_qty']) ||
(!empty($item['it_buy_max_qty']) && $base_qty > $item['it_buy_max_qty']))
return array('error' => '상품의 선택옵션과 최소/최대 구매수량을 확인해 주십시오.');
return array('error' => '', 'rows' => $validated, 'total' => $total);
}
function shop_cart_option_data($it_id)
{
global $g5;
$id = sql_escape_string($it_id);
$item = sql_fetch(" select * from {$g5['g5_shop_item_table']} where it_id = '$id' ");
$result = sql_query(" select * from {$g5['g5_shop_item_option_table']} where it_id = '$id' ");
$options = array();
while ($row = sql_fetch_array($result))
$options[] = $row;
return array($item, $options);
}
// 모든 상품을 먼저 검증한다. 바로구매/옵션수정의 선삭제와 다중 상품의 부분 저장을 막는다.
function shop_validate_cart_request($post, $multi = false)
{
$error = '장바구니 요청 정보가 올바르지 않습니다.';
$products = array();
if (empty($post['it_id']) || !is_array($post['it_id']) ||
array_keys($post['it_id']) !== range(0, count($post['it_id']) - 1))
return array('error' => $error);
foreach ($post['it_id'] as $i => $id) {
if ($multi && empty($post['chk_it_id'][$i]))
continue;
if (!is_string($id) || $id === '' || safe_replace_regex($id, 'it_id') !== $id || isset($products[$id]))
return array('error' => $error);
foreach (array('io_id', 'io_type', 'ct_qty') as $field) {
if (!isset($post[$field][$id]) || !is_array($post[$field][$id]) || !$post[$field][$id])
return array('error' => $error);
}
$keys = range(0, count($post['io_id'][$id]) - 1);
foreach (array('io_id', 'io_type', 'ct_qty') as $field) {
if (array_keys($post[$field][$id]) !== $keys)
return array('error' => $error);
}
$rows = array();
foreach ($keys as $k) {
$io_id = $post['io_id'][$id][$k];
if (!is_string($io_id) || preg_replace(G5_OPTION_ID_FILTER, '', $io_id) !== $io_id ||
(isset($post['io_value'][$id][$k]) && !is_string($post['io_value'][$id][$k])))
return array('error' => $error);
$rows[] = array('io_id' => $io_id, 'io_type' => $post['io_type'][$id][$k], 'ct_qty' => $post['ct_qty'][$id][$k]);
}
list($item, $options) = shop_cart_option_data($id);
$validated = shop_validate_cart_rows($item, $options, $rows);
if ($validated['error'] !== '')
return $validated;
$validated['item'] = $item;
$products[$id] = $validated;
}
return array('error' => $products ? '' : $error, 'products' => $products);
}
// 주문 준비/확정 시에는 선택된 행만 검증하여 보조옵션 단독 선택과 과거 조작 행도 차단한다.
function shop_validate_order_cart($cart_id)
{
global $g5;
$id = sql_escape_string($cart_id);
$result = sql_query(" select * from {$g5['g5_shop_cart_table']} where od_id = '$id' and ct_select = '1' order by ct_id asc ");
$products = array();
while ($row = sql_fetch_array($result)) {
if ($row['ct_status'] !== '쇼핑')
return '이미 처리되었거나 올바르지 않은 장바구니입니다.';
$products[$row['it_id']][] = $row;
}
if (!$products)
return '주문하실 상품을 선택해 주십시오.';
foreach ($products as $it_id => $rows) {
list($item, $options) = shop_cart_option_data($it_id);
$validated = shop_validate_cart_rows($item, $options, $rows, true);
if ($validated['error'] !== '')
return $validated['error'];
}
return '';
}