[KVE-2026-0608]그누보드5 Race Condition 취약점 수정

- bbs/poll_update.php: 설문조사 중복 투표 방지 (FIND_IN_SET을 WHERE에 포함한 원자적 UPDATE)
- bbs/good.php: 추천/비추천 카운터 부풀리기 방지 (INSERT IGNORE 우선 후 카운터 증가)
- lib/common.lib.php: insert_point() 포인트 중복 지급 방지 (MySQL named lock GET_LOCK 사용)
This commit is contained in:
thisgun
2026-04-16 01:07:17 +00:00
parent ddcc82d89b
commit 7e65a297bf
3 changed files with 52 additions and 46 deletions
+19 -8
View File
@@ -76,16 +76,22 @@ if(isset($_POST['js']) && $_POST['js'] === "on") {
}
else
{
// 추천(찬성), 비추천(반대) 카운트 증가
// 레이스 컨디션 방지: g5_board_good 테이블의 UNIQUE KEY(bo_table, wr_id, mb_id)를
// 이용해 INSERT IGNORE를 먼저 수행하고, 성공한 경우에만 카운터를 증가시킨다.
sql_query(" insert ignore into {$g5['board_good_table']} set bo_table = '{$bo_table}', wr_id = '{$wr_id}', mb_id = '{$member['mb_id']}', bg_flag = '{$good}', bg_datetime = '".G5_TIME_YMDHIS."' ");
if (get_sql_affected_rows() <= 0) {
$error = '이미 추천 또는 비추천 하신 글 입니다.';
print_result($error, $count);
}
// INSERT 성공 시에만 카운터 증가
sql_query(" update {$g5['write_prefix']}{$bo_table} set wr_{$good} = wr_{$good} + 1 where wr_id = '{$wr_id}' ");
// 내역 생성
sql_query(" insert {$g5['board_good_table']} set bo_table = '{$bo_table}', wr_id = '{$wr_id}', mb_id = '{$member['mb_id']}', bg_flag = '{$good}', bg_datetime = '".G5_TIME_YMDHIS."' ");
$sql = " select wr_{$good} as count from {$g5['write_prefix']}{$bo_table} where wr_id = '$wr_id' ";
$row = sql_fetch($sql);
$count = $row['count'];
run_event('bbs_increase_good_json', $bo_table, $wr_id, $good);
print_result($error, $count);
@@ -140,10 +146,15 @@ if(isset($_POST['js']) && $_POST['js'] === "on") {
}
else
{
// 추천(찬성), 비추천(반대) 카운트 증가
// 레이스 컨디션 방지: g5_board_good 테이블의 UNIQUE KEY(bo_table, wr_id, mb_id)를
// 이용해 INSERT IGNORE를 먼저 수행하고, 성공한 경우에만 카운터를 증가시킨다.
sql_query(" insert ignore into {$g5['board_good_table']} set bo_table = '{$bo_table}', wr_id = '{$wr_id}', mb_id = '{$member['mb_id']}', bg_flag = '{$good}', bg_datetime = '".G5_TIME_YMDHIS."' ");
if (get_sql_affected_rows() <= 0) {
alert('이미 추천 또는 비추천 하신 글 입니다.');
}
// INSERT 성공 시에만 카운터 증가
sql_query(" update {$g5['write_prefix']}{$bo_table} set wr_{$good} = wr_{$good} + 1 where wr_id = '{$wr_id}' ");
// 내역 생성
sql_query(" insert {$g5['board_good_table']} set bo_table = '{$bo_table}', wr_id = '{$wr_id}', mb_id = '{$member['mb_id']}', bg_flag = '{$good}', bg_datetime = '".G5_TIME_YMDHIS."' ");
if ($good == 'good')
$status = '추천';
@@ -151,7 +162,7 @@ if(isset($_POST['js']) && $_POST['js'] === "on") {
$status = '비추천';
$href = get_pretty_url($bo_table, $wr_id);
run_event('bbs_increase_good_html', $bo_table, $wr_id, $good, $href);
alert("이 글을 $status 하셨습니다.", '', false);
+19 -37
View File
@@ -14,50 +14,32 @@ $gb_poll = isset($_POST['gb_poll']) ? preg_replace('/[^0-9]/', '', $_POST['gb_po
if(!$gb_poll)
alert_close('항목을 선택하세요.');
$search_mb_id = false;
$search_ip = false;
if($is_member) {
// 투표했던 회원아이디들 중에서 찾아본다
$ids = explode(',', trim($po['mb_ids']));
for ($i=0; $i<count($ids); $i++) {
if ($member['mb_id'] == trim($ids[$i])) {
$search_mb_id = true;
break;
}
}
} else {
// 투표했던 ip들 중에서 찾아본다
$ips = explode(',', trim($po['po_ips']));
for ($i=0; $i<count($ips); $i++) {
if ($_SERVER['REMOTE_ADDR'] == trim($ips[$i])) {
$search_ip = true;
break;
}
}
}
$post_skin_dir = isset($_POST['skin_dir']) ? clean_xss_tags($_POST['skin_dir'], 1, 1) : '';
$result_url = G5_BBS_URL."/poll_result.php?po_id=$po_id&skin_dir={$post_skin_dir}";
// 없다면 선택한 투표항목을 1증가 시키고 ip, id를 저장
if (!($search_ip || $search_mb_id)) {
$po_ips = $po['po_ips'] . $_SERVER['REMOTE_ADDR'].",";
$mb_ids = $po['mb_ids'];
if ($is_member) { // 회원일 때는 id만 추가
$mb_ids .= $member['mb_id'].',';
$sql = " update {$g5['poll_table']} set po_cnt{$gb_poll} = po_cnt{$gb_poll} + 1, mb_ids = '$mb_ids' where po_id = '$po_id' ";
} else {
$sql = " update {$g5['poll_table']} set po_cnt{$gb_poll} = po_cnt{$gb_poll} + 1, po_ips = '$po_ips' where po_id = '$po_id' ";
}
sql_query($sql);
// 레이스 컨디션 방지: MyISAM은 트랜잭션을 지원하지 않으므로,
// WHERE 조건에 중복 검증(FIND_IN_SET)을 포함한 원자적 UPDATE로 처리한다.
// 기존 투표자 목록에 없을 때만 갱신되며, 동시 요청이 와도 DB 서버 레벨에서 직렬화된다.
if ($is_member) {
$sql = " update {$g5['poll_table']}
set po_cnt{$gb_poll} = po_cnt{$gb_poll} + 1,
mb_ids = concat(ifnull(mb_ids, ''), '".$member['mb_id'].",')
where po_id = '$po_id'
and find_in_set('".$member['mb_id']."', ifnull(mb_ids, '')) = 0 ";
} else {
$sql = " update {$g5['poll_table']}
set po_cnt{$gb_poll} = po_cnt{$gb_poll} + 1,
po_ips = concat(ifnull(po_ips, ''), '".$_SERVER['REMOTE_ADDR'].",')
where po_id = '$po_id'
and find_in_set('".$_SERVER['REMOTE_ADDR']."', ifnull(po_ips, '')) = 0 ";
}
sql_query($sql);
if (get_sql_affected_rows() <= 0) {
alert(addcslashes($po['po_subject'], '"\\/').'에 이미 참여하셨습니다.', $result_url);
}
if (!$search_mb_id)
insert_point($member['mb_id'], $po['po_point'], $po['po_id'] . '. ' . cut_str($po['po_subject'],20) . ' 투표 참여 ', '@poll', $po['po_id'], '투표');
insert_point($member['mb_id'], $po['po_point'], $po['po_id'] . '. ' . cut_str($po['po_subject'],20) . ' 투표 참여 ', '@poll', $po['po_id'], '투표');
//goto_url($g5['bbs_url'].'/poll_result.php?po_id='.$po_id.'&amp;skin_dir='.$skin_dir);
goto_url($result_url);
+14 -1
View File
@@ -1083,16 +1083,24 @@ function insert_point($mb_id, $point, $content='', $rel_table='', $rel_id='', $r
$mb_point = get_point_sum($mb_id);
// 이미 등록된 내역이라면 건너뜀
// 레이스 컨디션 방지: MyISAM은 트랜잭션을 지원하지 않으므로 MySQL named lock(GET_LOCK)으로
// 검증/INSERT 구간을 직렬화한다. rel 키가 없는 일반 포인트 지급은 락 대상이 아니다.
$point_lock_name = '';
if ($rel_table || $rel_id || $rel_action)
{
$point_lock_name = 'g5pt_' . md5($mb_id.'|'.$rel_table.'|'.$rel_id.'|'.$rel_action);
sql_fetch(" select get_lock('$point_lock_name', 5) as got_lock ");
$sql = " select count(*) as cnt from {$g5['point_table']}
where mb_id = '$mb_id'
and po_rel_table = '$rel_table'
and po_rel_id = '$rel_id'
and po_rel_action = '$rel_action' ";
$row = sql_fetch($sql);
if ($row['cnt'])
if ($row['cnt']) {
sql_query(" select release_lock('$point_lock_name') ");
return -1;
}
}
// 포인트 건별 생성
@@ -1134,6 +1142,11 @@ function insert_point($mb_id, $point, $content='', $rel_table='', $rel_id='', $r
$sql = " update {$g5['member_table']} set mb_point = '$po_mb_point' where mb_id = '$mb_id' ";
sql_query($sql);
// named lock 해제
if ($point_lock_name) {
sql_query(" select release_lock('$point_lock_name') ");
}
return 1;
}