변경 파일 목록
총 1개
Component (1)
Validator.phpValidator/변경 파일 코드
추가삭제
변경 전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; | |