security: KVE-2026-2344 관리자 날짜 필터 검증과 속성 인코딩으로 XSS 차단

보관함과 상품판매순위에서 날짜 전체 형식·실제 달력 날짜·기간 순서를 검증하고 잘못된 요청을 조회 전에 HTTP 400으로 거부한다. 날짜 출력과 조회 링크를 각각 인코딩하고 최초 조회·분류·정렬·페이징을 유지한다.

검증: 변경 PHP 문법 검사, 100개 잘못된 날짜 입력과 정상 조회·윤년·페이징 회귀 검사, Chromium 속성 주입 및 이벤트 차단 검사 통과. 실제 관리자 세션과 운영 DB 검증 및 배포는 미실행.
This commit is contained in:
whitedot
2026-09-09 07:58:16 +00:00
parent ded3349bfc
commit ff8e8741b3
6 changed files with 277 additions and 14 deletions
+26
View File
@@ -0,0 +1,26 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// 날짜 없는 최초 조회와 잘못된 날짜 조회를 구분한다. 오류는 false로 반환한다.
function shop_admin_date_range($params, $default_to = '')
{
if (!array_key_exists('fr_date', $params) && !array_key_exists('to_date', $params)) {
return array('', $default_to);
}
foreach (array('fr_date', 'to_date') as $key) {
if (!isset($params[$key]) || !is_string($params[$key]) || !preg_match('/\A[0-9]{8}\z/', $params[$key])) {
return false;
}
$date = $params[$key];
if (!checkdate((int) substr($date, 4, 2), (int) substr($date, 6, 2), (int) substr($date, 0, 4))) {
return false;
}
}
if ($params['fr_date'] > $params['to_date']) {
return false;
}
return array($params['fr_date'], $params['to_date']);
}
+19 -7
View File
@@ -4,13 +4,18 @@ include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
include_once('./date_filter.lib.php');
$date_range = shop_admin_date_range($_GET, date('Ymd'));
if ($date_range === false) {
http_response_code(400);
alert('조회 기간은 올바른 시작일과 종료일을 YYYYMMDD 형식으로 입력해 주십시오. 시작일은 종료일보다 늦을 수 없습니다.');
}
list($fr_date, $to_date) = $date_range;
$g5['title'] = '상품판매순위';
include_once (G5_ADMIN_PATH.'/admin.head.php');
include_once(G5_PLUGIN_PATH.'/jquery-ui/datepicker.php');
$fr_date = (isset($_GET['fr_date']) && preg_match("/[0-9]/", $_GET['fr_date'])) ? $_GET['fr_date'] : '';
$to_date = (isset($_GET['to_date']) && preg_match("/[0-9]/", $_GET['to_date'])) ? $_GET['to_date'] : date("Ymd", time());
$doc = isset($_GET['doc']) ? clean_xss_tags($_GET['doc'], 1, 1) : '';
$sort1 = (isset($_GET['sort1']) && in_array($_GET['sort1'], array('ct_status_1', 'ct_status_2', 'ct_status_3', 'ct_status_4', 'ct_status_5', 'ct_status_6', 'ct_status_7', 'ct_status_8', 'ct_status_9', 'ct_status_sum'))) ? $_GET['sort1'] : 'ct_status_sum';
$sort2 = (isset($_GET['sort2']) && in_array($_GET['sort2'], array('desc', 'asc'))) ? $_GET['sort2'] : 'desc';
@@ -57,7 +62,14 @@ $sql = $sql . " limit $from_record, $rows ";
$result = sql_query($sql);
//$qstr = 'page='.$page.'&amp;sort1='.$sort1.'&amp;sort2='.$sort2;
$qstr1 = $qstr.'&amp;fr_date='.$fr_date.'&amp;to_date='.$to_date.'&amp;sel_ca_id='.$sel_ca_id;
// 검증된 날짜만 URL에 전달한다. 최초 조회의 빈 날짜는 링크에서 생략한다.
$filter_query = array('sel_ca_id' => $sel_ca_id);
if ($fr_date !== '' && $to_date !== '') {
$filter_query['fr_date'] = $fr_date;
$filter_query['to_date'] = $to_date;
}
$qstr1 = htmlspecialchars(http_build_query($filter_query, '', '&'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$paging_query = htmlspecialchars(http_build_query(array_merge($filter_query, array('sort1' => $sort1, 'sort2' => $sort2)), '', '&'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$listall = '<a href="'.$_SERVER['SCRIPT_NAME'].'" class="ov_listall">전체목록</a>';
?>
@@ -92,9 +104,9 @@ $listall = '<a href="'.$_SERVER['SCRIPT_NAME'].'" class="ov_listall">전체목
기간설정
<label for="fr_date" class="sound_only">시작일</label>
<input type="text" name="fr_date" value="<?php echo $fr_date; ?>" id="fr_date" required class="required frm_input" size="8" maxlength="8"> 에서
<input type="text" name="fr_date" value="<?php echo htmlspecialchars($fr_date, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>" id="fr_date" required class="required frm_input" size="8" maxlength="8"> 에서
<label for="to_date" class="sound_only">종료일</label>
<input type="text" name="to_date" value="<?php echo $to_date; ?>" id="to_date" required class="required frm_input" size="8" maxlength="8"> 까지
<input type="text" name="to_date" value="<?php echo htmlspecialchars($to_date, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>" id="to_date" required class="required frm_input" size="8" maxlength="8"> 까지
<input type="submit" value="검색" class="btn_submit">
</form>
@@ -162,7 +174,7 @@ $listall = '<a href="'.$_SERVER['SCRIPT_NAME'].'" class="ov_listall">전체목
</table>
</div>
<?php echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, "{$_SERVER['SCRIPT_NAME']}?$qstr1&amp;page="); ?>
<?php echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, "{$_SERVER['SCRIPT_NAME']}?$paging_query&amp;page="); ?>
<script>
$(function() {
+19 -7
View File
@@ -4,13 +4,18 @@ include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
include_once('./date_filter.lib.php');
$date_range = shop_admin_date_range($_GET);
if ($date_range === false) {
http_response_code(400);
alert('조회 기간은 올바른 시작일과 종료일을 YYYYMMDD 형식으로 입력해 주십시오. 시작일은 종료일보다 늦을 수 없습니다.');
}
list($fr_date, $to_date) = $date_range;
$g5['title'] = '보관함현황';
include_once (G5_ADMIN_PATH.'/admin.head.php');
include_once(G5_PLUGIN_PATH.'/jquery-ui/datepicker.php');
$fr_date = (isset($_GET['fr_date']) && preg_match("/[0-9]/", $_GET['fr_date'])) ? $_GET['fr_date'] : '';
$to_date = (isset($_GET['to_date']) && preg_match("/[0-9]/", $_GET['to_date'])) ? $_GET['to_date'] : '';
$doc = isset($_GET['doc']) ? clean_xss_tags($_GET['doc'], 1, 1) : '';
$sort1 = (isset($_GET['sort1']) && in_array($_GET['sort1'], array('mb_id', 'it_id', 'wi_time', 'wi_ip'))) ? $_GET['sort1'] : 'it_id_cnt';
$sort2 = (isset($_GET['sort2']) && in_array($_GET['sort2'], array('desc', 'asc'))) ? $_GET['sort2'] : 'desc';
@@ -47,7 +52,14 @@ $rank = ($page - 1) * $rows;
$sql = $sql . " limit $from_record, $rows ";
$result = sql_query($sql);
$qstr1 = $qstr.'&amp;fr_date='.$fr_date.'&amp;to_date='.$to_date.'&amp;sel_ca_id='.$sel_ca_id;
// 검증된 날짜만 URL에 전달한다. 최초 조회의 빈 날짜는 링크에서 생략한다.
$filter_query = array('sel_ca_id' => $sel_ca_id);
if ($fr_date !== '' && $to_date !== '') {
$filter_query['fr_date'] = $fr_date;
$filter_query['to_date'] = $to_date;
}
$qstr1 = htmlspecialchars(http_build_query($filter_query, '', '&'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$paging_query = htmlspecialchars(http_build_query(array_merge($filter_query, array('sort1' => $sort1, 'sort2' => $sort2)), '', '&'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$listall = '<a href="'.$_SERVER['SCRIPT_NAME'].'" class="ov_listall">전체목록</a>';
?>
@@ -79,10 +91,10 @@ $listall = '<a href="'.$_SERVER['SCRIPT_NAME'].'" class="ov_listall">전체목
</select>
<label for="fr_date" class="sound_only">시작일</label>
<input type="text" name="fr_date" value="<?php echo $fr_date; ?>" id="fr_date" required class="required frm_input" size="8" maxlength="8">
<input type="text" name="fr_date" value="<?php echo htmlspecialchars($fr_date, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>" id="fr_date" required class="required frm_input" size="8" maxlength="8">
~
<label for="to_date" class="sound_only">종료일</label>
<input type="text" name="to_date" value="<?php echo $to_date; ?>" id="to_date" required class="required frm_input" size="8" maxlength="8">
<input type="text" name="to_date" value="<?php echo htmlspecialchars($to_date, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>" id="to_date" required class="required frm_input" size="8" maxlength="8">
<input type="submit" value="검색" class="btn_submit">
</form>
@@ -127,7 +139,7 @@ $listall = '<a href="'.$_SERVER['SCRIPT_NAME'].'" class="ov_listall">전체목
</table>
</div>
<?php echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, "{$_SERVER['SCRIPT_NAME']}?$qstr1&amp;page="); ?>
<?php echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, "{$_SERVER['SCRIPT_NAME']}?$paging_query&amp;page="); ?>
<div class="local_desc01 local_desc">
<p>고객님들이 보관함에 가장 많이 넣은 순으로 순위를 출력합니다.</p>