[security]KVE-2026-0882 주문 포인트 차감 Race Condition (Double Spend) 수정

shop/orderformupdate.php / mobile/shop/orderformupdate.php 의 포인트
검증과 차감 사이에 TOCTOU race 가 존재하여, 동일 회원이 여러 세션으로
동시에 주문 제출 시 같은 포인트 잔액을 반복 검증 통과 → 다중 차감으로
mb_point 가 음수가 되는 double spend 가 가능했음.

insert_point() 의 기존 named lock 은 (mb_id, rel_table, rel_id, rel_action)
조합 키 기반이라 서로 다른 od_id 주문 간에는 lock 이 다르고, 주문 차감
호출에 rel_* 가 전달되지 않아 lock 미적용 상태였음.

조치: KVE-2026-0687 (쇼핑몰 쿠폰) 와 동일한 회원 단위 MySQL GET_LOCK
패턴을 적용. lock 획득 후 g5_point SUM 으로 잔액을 재조회하여
- 충분하면 정상 차감
- race 로 부족하면 사용 가능한 만큼만 차감 + 부족분은 od_receipt_point
  보정 + od_misu(미수금) 으로 반영하여 매장 손실 방지.

MyISAM 정책상 트랜잭션 금지이므로 named lock 으로 직렬화하는 방식이
가장 적합. 데스크톱·모바일 동일 패턴 적용.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thisgun
2026-05-26 06:52:25 +00:00
co-authored by Claude Opus 4.7
parent 6b2f9e094c
commit 56b1958d37
2 changed files with 54 additions and 4 deletions
+27 -2
View File
@@ -704,8 +704,33 @@ if(!$result) {
}
// 회원이면서 포인트를 사용했다면 테이블에 사용을 추가
if ($is_member && $od_receipt_point)
insert_point($member['mb_id'], (-1) * $od_receipt_point, "주문번호 $od_id 결제");
// 동시 주문 race condition 방지 — 회원 단위 GET_LOCK + 잔액 재조회
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 ");
if (!empty($lock_row['lk'])) {
$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' ");
}
sql_query(" do release_lock('$point_lock_key') ");
}
}
$od_memo = nl2br(htmlspecialchars2(stripslashes($od_memo))) . "&nbsp;";