feat: 웹쉘 공격 방어 강화
- 위험한 확장자 목록 확장 (50+ 확장자) - 이중 확장자 탐지 (test.php.jpg → php 감지) - finfo를 이용한 실제 MIME 타입 검증 - 파일 헤더 스캔으로 PHP/스크립트 시그니처 탐지 - Content-Type 스푸핑 공격 방어 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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', // <?xml 제외
|
||||
'/<script[^>]*language\s*=\s*["\']?php/i',
|
||||
'/<%\s/', // ASP 태그
|
||||
'/<jsp:/i',
|
||||
];
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $header)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 파일 여부 확인
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user