[KVE-2026-1806] 콘텐츠 include 경로 및 미완료주문 출력 처리 보강

콘텐츠 상단/하단 파일 경로를 저장 전에 먼저 정규화한 뒤 확장자와 위치를
검증하도록 순서를 조정한다. 정규화 과정에서 확장자가 사라져 검증을
우회하던 경로를 차단한다. 실행 직전 bbs/content.php 에서도 승인 확장자와
data 디렉터리 귀결 여부를 다시 확인하는 is_content_include_allowed() 를
적용하고, 콘텐츠 상단/하단 이미지 업로드는 이미지 파일만 허용하도록 검사한다.

미완료주문 상세의 결제방법 출력에 get_text() 인코딩을 적용하고, 임시주문
저장 시 정제한 결제수단 값을 원본에도 반영한다. 개인결제 주문 세션은
결제정보 검증을 통과한 뒤에만 설정한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 196255d76912531d7e13861821bd59ca15c967d1)
This commit is contained in:
thisgun
2026-08-24 07:44:22 +00:00
parent 35acbfa750
commit 6571119db6
6 changed files with 65 additions and 10 deletions
+28 -5
View File
@@ -40,6 +40,12 @@ if ($is_admin !== 'super') {
$co_include_tail = '';
}
}
// 저장·검증 전에 경로를 먼저 정규화하여, 검증 이후 경로가 달라지지 않도록 한다.
if (function_exists('filter_input_include_path')) {
$co_include_head = filter_input_include_path($co_include_head);
$co_include_tail = filter_input_include_path($co_include_tail);
}
$co_tag_filter_use = isset($_POST['co_tag_filter_use']) ? (int) $_POST['co_tag_filter_use'] : 1;
$co_himg_del = (isset($_POST['co_himg_del']) && $_POST['co_himg_del']) ? 1 : 0;
$co_timg_del = (isset($_POST['co_timg_del']) && $_POST['co_timg_del']) ? 1 : 0;
@@ -86,6 +92,14 @@ if ($co_include_tail) {
}
}
if ($co_include_head && function_exists('is_content_include_allowed') && !is_content_include_allowed($co_include_head)) {
alert('상단 파일 경로로 사용할 수 없는 위치입니다.');
}
if ($co_include_tail && function_exists('is_content_include_allowed') && !is_content_include_allowed($co_include_tail)) {
alert('하단 파일 경로로 사용할 수 없는 위치입니다.');
}
if ($co_include_head && !is_include_path_check($co_include_head, 1)) {
$co_include_head = '';
$error_msg = '/data/file/ 또는 /data/editor/ 포함된 문자를 상단 파일 경로에 포함시킬수 없습니다.';
@@ -96,11 +110,6 @@ if ($co_include_tail && !is_include_path_check($co_include_tail, 1)) {
$error_msg = '/data/file/ 또는 /data/editor/ 포함된 문자를 하단 파일 경로에 포함시킬수 없습니다.';
}
if (function_exists('filter_input_include_path')) {
$co_include_head = filter_input_include_path($co_include_head);
$co_include_tail = filter_input_include_path($co_include_tail);
}
$co_seo_title = exist_seo_title_recursive('content', generate_seo_title($co_subject), $g5['content_table'], $co_id);
$sql_common = " co_include_head = '$co_include_head',
@@ -147,6 +156,20 @@ if (function_exists('get_admin_captcha_by')) {
g5_delete_cache_by_prefix('content-' . $co_id . '-');
if ($w == "" || $w == "u") {
foreach (array('co_himg', 'co_timg') as $content_img_field) {
if (empty($_FILES[$content_img_field]['name']) || empty($_FILES[$content_img_field]['tmp_name']))
continue;
// 상단/하단 이미지 슬롯에는 이미지만 허용하고, 실행 가능한 태그가 담긴 파일은 거부한다.
$tmp_file = $_FILES[$content_img_field]['tmp_name'];
$is_valid_image = @getimagesize($tmp_file);
$head_bytes = @file_get_contents($tmp_file, false, null, 0, 8192);
if ($is_valid_image === false || ($head_bytes !== false && preg_match('/<\?php|<\?=|<\?|<script/i', $head_bytes))) {
alert('상단/하단 이미지에는 이미지 파일만 등록할 수 있습니다.');
}
}
if ($_FILES['co_himg']['name']) {
$dest_path = G5_DATA_PATH . "/content/" . $co_id . "_h";
@move_uploaded_file($_FILES['co_himg']['tmp_name'], $dest_path);
+1 -1
View File
@@ -367,7 +367,7 @@ $pg_anchor = '<ul class="anchor">
<tbody>
<tr>
<td><?php echo $od['od_id']; ?></td>
<td class="td_paybybig"><?php echo $s_receipt_way; ?></td>
<td class="td_paybybig"><?php echo get_text($s_receipt_way); ?></td>
<td class="td_numbig td_numsum"><?php echo display_price($amount['order']); ?></td>
<td class="td_numbig"><?php echo display_price($od_send_cost + $od_send_cost2); ?></td>
<td class="td_numbig"><?php echo display_point($od_temp_point); ?></td>
+4 -2
View File
@@ -31,7 +31,8 @@ if (! (isset($co['co_id']) && $co['co_id']))
$g5['title'] = $co['co_subject'];
if ($co['co_include_head'] && is_include_path_check($co['co_include_head']))
if ($co['co_include_head'] && is_include_path_check($co['co_include_head'])
&& (!function_exists('is_content_include_allowed') || is_content_include_allowed($co['co_include_head'])))
@include_once($co['co_include_head']);
else
include_once('./_head.php');
@@ -99,7 +100,8 @@ if(is_file($skin_file)) {
echo '<p>'.str_replace(G5_PATH.'/', '', $skin_file).'이 존재하지 않습니다.</p>';
}
if ($co['co_include_tail'] && is_include_path_check($co['co_include_tail']))
if ($co['co_include_tail'] && is_include_path_check($co['co_include_tail'])
&& (!function_exists('is_content_include_allowed') || is_content_include_allowed($co['co_include_tail'])))
@include_once($co['co_include_tail']);
else
include_once('./_tail.php');
+27
View File
@@ -4853,6 +4853,33 @@ function filter_input_include_path($path){
return str_replace('//', '/', strip_tags($path));
}
// 콘텐츠 상단/하단 include 대상이 템플릿 파일인지 실행 직전 재확인한다.
// 승인 확장자만 허용하고, 업로드가 저장되는 data 디렉터리로 귀결되는 경로는 거부한다.
function is_content_include_allowed($path){
if( !is_string($path) || $path === '' ){
return false;
}
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if( !in_array($ext, array('php', 'htm', 'html')) ){
return false;
}
$real = @realpath($path);
if( $real !== false && defined('G5_DATA_PATH') ){
$data_real = @realpath(G5_DATA_PATH);
if( $data_real !== false ){
$real_norm = str_replace('\\', '/', $real);
$data_norm = rtrim(str_replace('\\', '/', $data_real), '/').'/';
if( strpos($real_norm, $data_norm) === 0 ){
return false;
}
}
}
return true;
}
function option_array_checked($option, $arr=array()){
$checked = '';
+2
View File
@@ -15,6 +15,8 @@ sql_query($sql);
*/
$od_settle_case = isset($_POST['od_settle_case']) ? clean_xss_tags($_POST['od_settle_case'], 1, 1) : '';
// 저장되는 원본에도 정제한 값을 반영한다.
$_POST['od_settle_case'] = $od_settle_case;
if(isset($_POST['pp_id']) && $_POST['pp_id']) {
$od_id = get_session('ss_personalpay_id');
+3 -2
View File
@@ -16,8 +16,6 @@ if($default['de_pg_service'] == 'lg' && ! $post_lgd_paykey)
if($default['de_pg_service'] == 'toss' && ! $paymentKey)
alert('결제등록 요청 후 주문해 주십시오.', $page_return_url);
set_session('ss_order_id', $pp_id);
// 개인결제 정보
$pp_check = false;
$sql = " select * from {$g5['g5_shop_personalpay_table']} where pp_id = '{$pp_id}' and pp_use = '1' ";
@@ -32,6 +30,9 @@ $hash_data = md5($pp_id.$good_mny.$pp['pp_time']);
if($pp_id !== get_session('ss_personalpay_id') || $hash_data !== get_session('ss_personalpay_hash'))
die('개인결제 정보가 올바르지 않습니다.');
// 개인결제 정보 검증을 통과한 뒤에만 주문 세션을 설정한다.
set_session('ss_order_id', $pp_id);
// PG사의 가상계좌 또는 계좌이체의 자동 현금영수증 초기배열값
$pg_receipt_infos = array(
'od_cash' => 0,