본문으로 건너뛰기

상품 상세페이지 이미지 업로드 안정성 개선

PHP 8 · GODO25
변경 파일 수: 1개
변경 파일 목록
총 1개
변경 파일 코드
추가삭제
Component/Validator/Validator.php+38-9
변경 전Validator.php
const TEXT_USERID_INVALID = '%s은(는) 아이디형식에 맞지 않습니다.';
const TEXT_YN_INVALID = '%s은(는) y 또는 n 이어야 합니다.';
const TEXT_BOOLEAN_INVALID = '%s은(는) true 또는 false 이어야 합니다.';
 
 
 
 
 
/**
* act 함수 실행 시 데이터가 없는경우에도 체크하도록 하기 위한 플래그
* @var bool
}
 
/*
* PHP 열림 태그(<?php / <?=)부터 닫힘 태그(또는 파일 끝)까지 추출 — 닫힘 태그 우회 차단 + 바이너리 false positive 회피.
* 열림 태그를 <?php / <?= 로 한정: 바이너리에 우연히 나온 <? 가 거대한 가짜 PHP 블록을 만들어 정상 이미지를 오탐하던 문제 차단.
* /s 모디파이어로 . 가 newline 도 매칭하여 nl2br 트릭 불필요. /i 로 <?PHP 대소문자 무시.
 
 
 
 
 
*/
if (preg_match_all('/<\?(?:php|=)(.*?)(?:\?>|$)/si', $text, $matches)) {
$dangerPatterns = [
'variable_access' => [
'description' => '슈퍼글로벌 변수 및 변수 변수 접근',
'patterns' => [
'\$_(?:GET|POST|REQUEST|COOKIE|SERVER|FILES|ENV|SESSION)',
'\$\{\s*[\'"]?_(?:GET|POST|REQUEST|COOKIE|SERVER|FILES|ENV|SESSION)',
'\$\$\w+',
]
],
'code_execution' => [
 
$combinedPattern = '/' . implode('|', array_merge(...array_column($dangerPatterns, 'patterns'))) . '/i';
 
foreach ($matches[1] as $phpContent) {
if (preg_match($combinedPattern, $phpContent)) {
return false;
 
 
 
 
 
 
 
 
 
 
 
 
}
 
 
 
 
 
 
 
}
}
return true;
 
변경 후Validator.php
const TEXT_USERID_INVALID = '%s은(는) 아이디형식에 맞지 않습니다.';
const TEXT_YN_INVALID = '%s은(는) y 또는 n 이어야 합니다.';
const TEXT_BOOLEAN_INVALID = '%s은(는) true 또는 false 이어야 합니다.';
/**
* 웹쉘 검사 시 PHP 열림 태그 뒤로 훑을 최대 바이트 수
* 실제 페이로드는 태그 뒤 100바이트 안쪽에 나오고 범위를 넓힐수록 바이너리 오탐이 늘어, 4096 에서 절충
*/
const INCLUDE_EVAL_SCAN_WINDOW = 4096;
/**
* act 함수 실행 시 데이터가 없는경우에도 체크하도록 하기 위한 플래그
* @var bool
}
 
/*
* PHP 열림 태그 위치만 찾아, 각 태그 뒤 INCLUDE_EVAL_SCAN_WINDOW 바이트만 검사한다.
* 열림 태그부터 파일 끝까지를 한 블록으로 잡으면 바이너리에 우연히 섞인 3바이트 열림 태그
* 하나가 뒤쪽 전체를 검사 대상으로 끌고 와 정상 이미지를 오탐한다 (용량이 클수록 확률 증가).
* 정규식이 블록을 소비하지 않으므로, 이미지 뒤에 이어붙인 웹쉘의 열림 태그도 개별로 검사된다.
*
* 겹치는 윈도우는 하나의 구간으로 합쳐 한 번만 검사한다. 검사 대상 바이트의 합집합은 같지만,
* 열림 태그를 빽빽이 채운 파일에서 같은 구간을 태그 수만큼 다시 훑는 비용이 사라진다.
* 태그 위치를 한꺼번에 모으지 않고 흘려보내므로 태그 수가 많아도 메모리가 늘지 않는다.
*/
if (str_contains($text, '<?')) {
$dangerPatterns = [
'variable_access' => [
'description' => '슈퍼글로벌 변수 및 변수 변수 함수 호출',
'patterns' => [
'\$_(?:GET|POST|REQUEST|COOKIE|SERVER|FILES|ENV|SESSION)',
'\$\{\s*[\'"]?_(?:GET|POST|REQUEST|COOKIE|SERVER|FILES|ENV|SESSION)',
'\$\$\{?\w+\}?(?:\[[^\]\n]{0,64}\])?\s*\(',
]
],
'code_execution' => [
 
$combinedPattern = '/' . implode('|', array_merge(...array_column($dangerPatterns, 'patterns'))) . '/i';
 
$scanPattern = '/<\?(?:php|=)/i';
$rangeStart = null;
$rangeEnd = 0;
$offset = 0;
while (preg_match($scanPattern, $text, $openTag, PREG_OFFSET_CAPTURE, $offset)) {
// PREG_OFFSET_CAPTURE 오프셋은 int 지만 preg_match 스텁이 string[] 이라 분석기용 캐스팅
$tagOffset = (int) $openTag[0][1];
if ($rangeStart === null) {
$rangeStart = $tagOffset;
} elseif ($tagOffset > $rangeEnd) {
// 앞 구간과 떨어진 태그 — 앞 구간을 먼저 검사하고 새 구간을 시작한다
if (preg_match($combinedPattern, substr($text, $rangeStart, $rangeEnd - $rangeStart))) {
return false;
}
$rangeStart = $tagOffset;
}
$rangeEnd = $tagOffset + self::INCLUDE_EVAL_SCAN_WINDOW;
$offset = $tagOffset + 1;
}
 
if ($rangeStart !== null
&& preg_match($combinedPattern, substr($text, $rangeStart, $rangeEnd - $rangeStart))) {
return false;
}
}
return true;