FU: 14. 악성 파일 업로드
분류: Web Application(웹)
중요도: 상
개요
점검 내용
웹 애플리케이션 내 업로드 기능 이용 시 악성 파일의 업로드 및 실행 가능 여부 점검
점검 목적
업로드되는 파일의 확장자에 대한 적절성을 검증하는 로직을 구현하여 악성 파일(Server Side Script, exe, dll, bat 등)의 업로드를 방지하고, 서버에 저장된 파일 경로를 유추하여 해당 파일의 실행을 제한하기 위함
보안 위협
해당 취약점이 존재할 경우, 공격자는 악성 파일을 서버에 업로드 및 실행하여 시스템 관리자 권한을 획득하거나 인접 서버에 대한 침입을 시도할 수 있음
참고
Server Side Script
웹에서 사용되는 스크립트 언어 중 서버 측에서 실행되는 스크립트
악성 콘텐츠
Flash 파일이나 dll, bat, exe 실행 파일 등 악성코드가 포함될 수 있는 콘텐츠
업로드 기능 제한
기반시설 특성상 원칙적으로 업로드 기능을 제한해야 하나, 부득이하게 사용해야 하는 경우 특정 사용자만 허용된 확장자의 콘텐츠 파일을 업로드할 수 있도록 구현
참고
소스코드 및 취약점 점검 필요
점검 대상 및 판단 기준
대상
웹 애플리케이션 소스코드, 웹 애플리케이션 서버, 웹 방화벽
판단 기준
✅ 양호: 업로드되는 파일에 대한 확장자 검증이 이루어지는 경우
❌ 취약: 업로드되는 파일에 대한 확장자 검증이 이루어지지 않고 업로드 경로 접근 시 정상적으로 실행이 가능한 경우
조치 방법
업로드되는 파일에 대한 확장자 검증 및 실행 권한 제거
조치 시 영향
일반적인 경우 영향 없음
점검 및 조치 사례
악성 콘텐츠 업로드
점검 방법
Step 1) 파일 업로드 기능 이용 시 파일 확장자(.exe, .bat, .sh, .dll 등) 검증 여부 확인
[ 악성 파일 업로드 시도 ]
Step 2) 임의 악성 파일에 대하여 정상적으로 업로드 가능 여부 확인
[ 악성 파일 업로드 유무 확인 ]
Step 3) 파일 다운로드 및 특정 서비스 간 실행 기능 제공 시 클라이언트/서버에 대하여 악성 코드 감염 가능성 여부 확인
[ 악성 파일에 대한 익스플로잇 및 실행 가능성 여부 확인 ]
Server Side Script 업로드
점검 방법
Step 1) Server Side Script 파일 업로드 및 파일 경로 확인
[ 업로드 된 악성 Server Side Script에 대한 경로 확인 ]
Step 2) 업로드된 Server Side Script 파일 경로 접근 시 파일 실행 여부 확인
[ 악성 Server Side Script 실행 유무 확인 ]
조치 방법
- 업로드 파일명에 인코딩/디코딩, 널바이트, 태그 등을 제거할 수 있도록 정규화 및 필터링
- 업로드 파일의 확장자 및 MIME 타입에 대해 화이트리스트 방식으로 검증 로직을 구현하여 서버 사이드로 하여금 허용된 파일 유형만 업로드를 허용하며 대용량 파일 업로드 시 용량 제한 구현
- 업로드된 파일의 이름을 암호화 후 저장하여 파일 이름을 유추할 수 없도록 처리
- 업로드된 파일의 실행 권한을 제한하여 해당 파일이 서버 사이드에서 실행되지 않도록 설정
- 업로드 경로에 대하여 웹 디렉터리와 격리 조치
- 주기적으로 업로드된 파일을 대상으로 바이러스 검사 실시
Java 파일 업로드 보안 코드
화이트리스트 방식의 확장자 검증 및 MIME 타입 검증 로직을 구현하여 허용된 유형의 파일만 업로드 허용
private static final String[] ALLOWED_EXTENSIONS = {"jpg", "png", "pdf", "txt"};
private static final Set<String> ALLOWED_MIME = Set.of("image/jpeg", "image/png", "application/pdf", "text/plain");
// 파일명 정규화
private static String normalizeFilename(String filename) {
if (filename == null) return null;
String name = java.net.URLDecoder.decode(filename, StandardCharsets.UTF_8);
name = Normalizer.normalize(name, Normalizer.Form.NFC);
name = name.replace("\0", "");
name = name.replaceAll("[<>:\"/\\\\|?*]", "");
name = name.replaceAll("^[.\\s]+|[.\\s]+$", "");
return name;
}
// 확장자 추출 + 이중 확장자 차단
private static String getExtension(String filename) {
String safe = normalizeFilename(filename);
int dotCount = safe.length() - safe.replace(".", "").length();
if (dotCount != 1) return ""; // 이중 확장자 차단
int idx = safe.lastIndexOf('.');
if (idx == -1) return "";
return safe.substring(idx+1).toLowerCase();
}
public static String saveFile(MultipartFile file, String uploadDir) throws IOException {
String original = file.getOriginalFilename();
String ext = getExtension(original);
if (!ALLOWED_EXTENSIONS.contains(ext)) {
throw new IOException("허용되지 않은 확장자");
}
// MIME 시그니처 검증
Tika tika = new Tika();
String mime = tika.detect(file.getInputStream());
if (!ALLOWED_MIME.contains(mime)) {
throw new IOException("허용되지 않은 파일 유형");
}
// 파일명 난수화
String newName = UUID.randomUUID().toString().replace("-", "") + "." + ext;
java.nio.file.Path savePath = java.nio.file.Paths.get(uploadDir, newName);
file.transferTo(savePath.toFile());
return newName;
}
ASP.NET 파일 업로드 보안 코드
화이트리스트 방식의 확장자 검증 로직을 통한 허용된 확장자 파일만 업로드
private static readonly string[] AllowedExtensions = { ".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt" };
private static readonly string[] AllowedMime = { "image/jpeg","image/png","image/gif","application/pdf", "text/plain" };
// 파일명 정규화
private static string NormalizeFilename(string filename) {
if (string.IsNullOrWhiteSpace(filename)) return string.Empty;
string name = System.Web.HttpUtility.UrlDecode(filename, Encoding.UTF8);
name = name.Normalize(NormalizationForm.FormC);
name = name.Replace("\0", "");
name = Regex.Replace(name, "[<>:\"/\\\\|?*]", "");
name = Regex.Replace(name, "^[.\\s]+|[.\\s]+$", "");
return name;
}
// 확장자 검증 (이중 확장자 차단 포함)
private static bool IsValidExtension(string filename) {
string safeName = NormalizeFilename(filename);
if (string.IsNullOrEmpty(safeName)) return false;
// 이중 확장자 차단
if (safeName.Split('.').Length != 2) return false;
string ext = Path.GetExtension(safeName).ToLowerInvariant();
return Array.Exists(AllowedExtensions, e => e == ext);
}
protected void btnUpload_Click(object sender, EventArgs e) {
if (FileUpload1.HasFile) {
string safeName = NormalizeFilename(FileUpload1.FileName);
// 확장자 검증
if (!IsValidExtension(safeName)) {
Response.Write("허용되지 않은 확장자입니다.");
return;
}
// MIME 타입 검증 (주의: Web Forms의 ContentType은 신뢰성이 낮음)
string mime = FileUpload1.PostedFile.ContentType.ToLowerInvariant();
if (Array.IndexOf(AllowedMime, mime) < 0) {
Response.Write("허용되지 않은 MIME 타입입니다.");
return;
}
// 난수화된 파일명 생성
string ext = Path.GetExtension(safeName).ToLowerInvariant();
string newName = Guid.NewGuid().ToString("N") + ext;
// 저장 경로 (웹 루트 외부 권장)
string uploadPath = Server.MapPath("~/Uploads/");
if (!Directory.Exists(uploadPath)) {
Directory.CreateDirectory(uploadPath);
}
string savePath = Path.Combine(uploadPath, newName);
try {
FileUpload1.SaveAs(savePath);
Response.Write("업로드 성공: " + HttpUtility.HtmlEncode(newName));
} catch (Exception ex) {
Response.Write("업로드 실패: " + HttpUtility.HtmlEncode(ex.Message));
}
} else {
Response.Write("업로드할 파일을 선택하세요.");
}
}
PHP 파일 업로드 보안 코드
move_uploaded_file의 경우, php.ini 파일 내 upload_tmp_dir 속성에 정의된 경로에 php***.tmp 형식의 임시 파일로 업로드되며, 유효하지 않은 파일의 경우 삭제 처리됨. 화이트리스트 방식의 확장자 검증 및 MIME 타입 검증 로직을 구현하여 허용된 유형의 파일만 업로드 허용
// 파일명 정규화
function normalize_filename($filename) {
$filename = urldecode($filename);
$filename = normalizer_normalize($filename, Normalizer::FORM_C);
$filename = str_replace("\0", "", $filename);
$filename = preg_replace('/[<>:"\/\\\\|?*]/', '', $filename);
$filename = preg_replace('/^[\.\s]+|[\.\s]+$/u', '', $filename);
return $filename;
}
// 이중 확장자 차단
function is_valid_extension($filename, $allowed_exts) {
$safe = normalize_filename($filename);
if (substr_count($safe, '.') !== 1) return false;
$ext = strtolower(pathinfo($safe, PATHINFO_EXTENSION));
return in_array($ext, $allowed_exts, true);
}
function save_upload($file, $uploadDir) {
$allowed_exts = ['jpg','jpeg','png','pdf','txt'];
$allowed_mime = ['image/jpeg','image/png','application/pdf','text/plain'];
// 확장자 검증
if (!is_valid_extension($file['name'], $allowed_exts)) {
throw new Exception("허용되지 않은 확장자");
}
// MIME 시그니처 확인
$mime = mime_content_type($file['tmp_name']);
if (!in_array($mime, $allowed_mime, true)) {
throw new Exception("허용되지 않은 파일 유형");
}
// 파일명 난수화
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$dest = rtrim($uploadDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $newName;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
throw new Exception("파일 저장 실패");
}
return $newName;
}