From 1d84bb603ec25e16760918db04c7a12c2f132ff0 Mon Sep 17 00:00:00 2001 From: kappa Date: Sat, 10 Jan 2026 15:10:53 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9B=B9=EC=89=98=20=EA=B3=B5=EA=B2=A9?= =?UTF-8?q?=20=EB=B0=A9=EC=96=B4=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 위험한 확장자 목록 확장 (50+ 확장자) - 이중 확장자 탐지 (test.php.jpg → php 감지) - finfo를 이용한 실제 MIME 타입 검증 - 파일 헤더 스캔으로 PHP/스크립트 시그니처 탐지 - Content-Type 스푸핑 공격 방어 Co-Authored-By: Claude Opus 4.5 --- extend/r2-storage/src/R2FileHandler.php | 96 ++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/extend/r2-storage/src/R2FileHandler.php b/extend/r2-storage/src/R2FileHandler.php index eee65e3..d138b71 100644 --- a/extend/r2-storage/src/R2FileHandler.php +++ b/extend/r2-storage/src/R2FileHandler.php @@ -127,6 +127,11 @@ class R2FileHandler return $this->handleUploadError($file['error']); } + // MIME 타입 검증 (Content-Type 스푸핑 방지) + if (!$this->validateMimeType($file['tmp_name'], $file['name'])) { + return ['success' => false, 'error' => 'Invalid file type or suspicious content detected']; + } + // R2 비활성화 시 로컬 저장 정보 반환 if (!$this->isEnabled()) { if ($this->fallbackToLocal) { @@ -235,9 +240,28 @@ class R2FileHandler // 확장자 추출 $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); - // 위험한 확장자 처리 - $dangerousExt = ['php', 'phtml', 'php3', 'php4', 'php5', 'phps', 'phar', 'inc', 'jsp', 'jspx', 'cgi', 'pl', 'py', 'asp', 'aspx']; - if (in_array($ext, $dangerousExt)) { + // 위험한 확장자 처리 (이중 확장자 포함) + $dangerousExt = [ + 'php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'php8', 'phps', 'phar', 'pht', 'pgif', + 'inc', 'hphp', 'ctp', + 'jsp', 'jspx', 'jsw', 'jsv', 'jspf', 'wss', 'do', 'action', + 'cgi', 'pl', 'py', 'pyc', 'pyo', 'rb', 'sh', 'bash', + 'asp', 'aspx', 'cer', 'asa', 'asax', 'ashx', 'asmx', 'axd', + 'cfm', 'cfml', 'cfc', + 'htaccess', 'htpasswd', 'ini', 'config', + 'exe', 'dll', 'bat', 'cmd', 'com', 'msi', 'scr', 'vbs', 'vbe', 'js', 'jse', 'wsf', 'wsh', + 'svg', // SVG can contain JavaScript + ]; + + // 확장자 소문자 변환 + $ext = strtolower($ext); + + // 이중 확장자 체크 (test.php.jpg → php 감지) + $basename = pathinfo($originalName, PATHINFO_FILENAME); + $innerExt = strtolower(pathinfo($basename, PATHINFO_EXTENSION)); + if (in_array($innerExt, $dangerousExt)) { + $ext = $innerExt . '_' . $ext . '-x'; + } elseif (in_array($ext, $dangerousExt)) { $ext .= '-x'; } @@ -459,6 +483,72 @@ class R2FileHandler return $this->adapter; } + /** + * MIME 타입 검증 (웹쉘 방지) + * + * @param string $filePath 임시 파일 경로 + * @param string $originalName 원본 파일명 + * @return bool + */ + private function validateMimeType(string $filePath, string $originalName): bool + { + if (!file_exists($filePath)) { + return false; + } + + // finfo로 실제 MIME 타입 확인 + if (function_exists('finfo_open')) { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $realMime = finfo_file($finfo, $filePath); + finfo_close($finfo); + + // PHP/스크립트 MIME 타입 차단 + $dangerousMimes = [ + 'application/x-php', + 'application/php', + 'application/x-httpd-php', + 'text/x-php', + 'text/php', + 'application/x-executable', + 'application/x-sharedlib', + 'application/x-shellscript', + 'text/x-shellscript', + 'text/x-python', + 'text/x-perl', + 'text/x-ruby', + ]; + + if (in_array($realMime, $dangerousMimes)) { + return false; + } + } + + // 파일 시작 부분 검사 (PHP 태그, 스크립트 시그니처) + $handle = fopen($filePath, 'rb'); + if ($handle) { + $header = fread($handle, 1024); + fclose($handle); + + // PHP 코드 시그니처 검사 + $patterns = [ + '/<\?php/i', + '/<\?=/i', + '/<\?[^x]/i', // ]*language\s*=\s*["\']?php/i', + '/<%\s/', // ASP 태그 + '/