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:
kappa
2026-01-10 15:10:53 +09:00
parent 7385eb8436
commit 1d84bb603e

View File

@@ -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;
}
/**
* 이미지 파일 여부 확인
*