From 4c00c6000634b1c8cc42c8b548337c6ac54418ca Mon Sep 17 00:00:00 2001 From: thisgun Date: Tue, 26 May 2026 07:10:53 +0000 Subject: [PATCH] =?UTF-8?q?[security]KVE-2026-0882=20race=20condition=20?= =?UTF-8?q?=EC=9E=94=EC=95=A1=20=EB=B6=80=EC=A1=B1=20=EC=8B=9C=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=20=EC=B7=A8=EC=86=8C=20=EC=B2=98=EB=A6=AC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이전 커밋(e53689ac3)의 미수금 처리 방식은 사용자에게 결제 금액과 다른 청구가 발생해 혼란/분쟁을 유발할 수 있어, 동시 주문 race 로 포인트 잔액이 부족하면 결제 자체를 취소하는 방식으로 변경. - PG 결제가 진행된 경우 (\$tno 존재): cancel_pg.inc.php 로 환불 요청 - 장바구니 복구: 기존 line 839 동일 패턴 (od_id = tmp_cart_id, ct_status = '쇼핑') - 주문 삭제: g5_shop_order_table 에서 od_id 제거 - 사용자에게 die 로 명확한 오류 메시지 표시 lock timeout 케이스도 동일하게 결제 취소 처리 (보수적). Co-Authored-By: Claude Opus 4.7 (1M context) --- mobile/shop/orderformupdate.php | 39 +++++++++++++++++++++++---------- shop/orderformupdate.php | 39 +++++++++++++++++++++++---------- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/mobile/shop/orderformupdate.php b/mobile/shop/orderformupdate.php index 4c11d57f0..172cc006f 100644 --- a/mobile/shop/orderformupdate.php +++ b/mobile/shop/orderformupdate.php @@ -754,27 +754,42 @@ if(!$result) { if ($is_member && $od_receipt_point) { $point_lock_key = 'g5pt_order_'.md5($member['mb_id']); $lock_row = sql_fetch(" select get_lock('$point_lock_key', 5) as lk "); + $lock_acquired = !empty($lock_row['lk']); + $point_shortage = false; - if (!empty($lock_row['lk'])) { + if ($lock_acquired) { $current_point = (int) get_point_sum($member['mb_id']); if ($current_point >= $od_receipt_point) { insert_point($member['mb_id'], (-1) * $od_receipt_point, "주문번호 $od_id 결제"); } else { - // race condition 으로 잔액 부족 — 사용 가능한 만큼만 차감하고 부족분은 미수금 처리 - $actual_point = $current_point > 0 ? $current_point : 0; - $shortage = $od_receipt_point - $actual_point; - - if ($actual_point > 0) { - insert_point($member['mb_id'], (-1) * $actual_point, "주문번호 $od_id 결제"); - } - sql_query(" update {$g5['g5_shop_order_table']} - set od_receipt_point = '$actual_point', - od_misu = od_misu + '$shortage' - where od_id = '$od_id' "); + $point_shortage = true; } sql_query(" do release_lock('$point_lock_key') "); + } else { + // lock timeout — 안전을 위해 결제 취소 처리 + $point_shortage = true; + } + + if ($point_shortage) { + // race condition 으로 잔액 부족 — PG 환불 + 장바구니 복구 + 주문 삭제 + if ($tno) { + $cancel_msg = '포인트 잔액 부족으로 결제 취소 (동시 주문 race)'; + include G5_SHOP_PATH.'/cancel_pg.inc.php'; + } + + // 장바구니 복구 + sql_query(" update {$g5['g5_shop_cart_table']} set od_id = '$tmp_cart_id', ct_status = '쇼핑' where od_id = '$od_id' ", false); + + // 주문 삭제 + sql_query(" delete from {$g5['g5_shop_order_table']} where od_id = '$od_id' "); + + if (function_exists('add_order_post_log')) { + add_order_post_log("동시 주문 race 로 인한 포인트 잔액 부족. 주문 $od_id 취소."); + } + + die('

회원님의 포인트 잔액이 부족하여 주문이 완료되지 않았습니다.

'.strtoupper($od_pg).'를 이용한 전자결제(신용카드, 계좌이체, 가상계좌 등)은 자동 취소되었습니다.

'); } } diff --git a/shop/orderformupdate.php b/shop/orderformupdate.php index 7d5912046..2bb75eebf 100644 --- a/shop/orderformupdate.php +++ b/shop/orderformupdate.php @@ -708,27 +708,42 @@ if(!$result) { if ($is_member && $od_receipt_point) { $point_lock_key = 'g5pt_order_'.md5($member['mb_id']); $lock_row = sql_fetch(" select get_lock('$point_lock_key', 5) as lk "); + $lock_acquired = !empty($lock_row['lk']); + $point_shortage = false; - if (!empty($lock_row['lk'])) { + if ($lock_acquired) { $current_point = (int) get_point_sum($member['mb_id']); if ($current_point >= $od_receipt_point) { insert_point($member['mb_id'], (-1) * $od_receipt_point, "주문번호 $od_id 결제"); } else { - // race condition 으로 잔액 부족 — 사용 가능한 만큼만 차감하고 부족분은 미수금 처리 - $actual_point = $current_point > 0 ? $current_point : 0; - $shortage = $od_receipt_point - $actual_point; - - if ($actual_point > 0) { - insert_point($member['mb_id'], (-1) * $actual_point, "주문번호 $od_id 결제"); - } - sql_query(" update {$g5['g5_shop_order_table']} - set od_receipt_point = '$actual_point', - od_misu = od_misu + '$shortage' - where od_id = '$od_id' "); + $point_shortage = true; } sql_query(" do release_lock('$point_lock_key') "); + } else { + // lock timeout — 안전을 위해 결제 취소 처리 + $point_shortage = true; + } + + if ($point_shortage) { + // race condition 으로 잔액 부족 — PG 환불 + 장바구니 복구 + 주문 삭제 + if ($tno) { + $cancel_msg = '포인트 잔액 부족으로 결제 취소 (동시 주문 race)'; + include G5_SHOP_PATH.'/cancel_pg.inc.php'; + } + + // 장바구니 복구 (기존 line 839 동일 패턴) + sql_query(" update {$g5['g5_shop_cart_table']} set od_id = '$tmp_cart_id', ct_status = '쇼핑' where od_id = '$od_id' ", false); + + // 주문 삭제 + sql_query(" delete from {$g5['g5_shop_order_table']} where od_id = '$od_id' "); + + if (function_exists('add_order_post_log')) { + add_order_post_log("동시 주문 race 로 인한 포인트 잔액 부족. 주문 $od_id 취소."); + } + + die('

회원님의 포인트 잔액이 부족하여 주문이 완료되지 않았습니다.

'.strtoupper($od_pg).'를 이용한 전자결제(신용카드, 계좌이체, 가상계좌 등)은 자동 취소되었습니다.

'); } }