악성파일 업로드 | 위험도: 5 (매우 높음) | 통제구분: 5.8.4 서비스 보호
인터넷뱅킹 - 금융상담 신청 시스템
재직증명서, 소득증빙서류 등을 첨부해주세요
업로드할 파일을 선택하세요
업로드 취약점을 통해 해커가 원격에서 웹 서버를 조종할 수 있도록 작성한 웹 스크립트
⚠️ 업로드 후 실행 시 시스템 명령어 실행 가능
// ❌ 취약한 코드 - 파일 검증 없음
@PostMapping("/upload")
public String uploadFile(
@RequestParam("file") MultipartFile file) {
// 파일명만 받아서 저장! (위험)
String fileName = file.getOriginalFilename();
String uploadPath = "/uploads/" + fileName;
// 확장자 검증 없이 저장
file.transferTo(new File(uploadPath));
return "success";
}
웹쉘이 업로드되어 서버가 장악되었습니다
공격자가 시스템 전체에 접근 가능합니다
// ✅ 안전한 코드 - 다층 검증
@PostMapping("/upload")
public String uploadFile(
@RequestParam("file") MultipartFile file)
throws Exception {
// 1. 확장자 화이트리스트 검증
String fileName = file.getOriginalFilename();
String extension = fileName.substring(
fileName.lastIndexOf(".") + 1).toLowerCase();
Set<String> allowedExtensions = Set.of(
"jpg", "jpeg", "png", "pdf", "hwp"
);
if (!allowedExtensions.contains(extension)) {
throw new SecurityException(
"허용되지 않은 확장자: " + extension);
}
// 2. 파일 MIME 타입 검증
String contentType = file.getContentType();
Set<String> allowedMimeTypes = Set.of(
"image/jpeg", "image/png",
"application/pdf", "application/x-hwp"
);
if (!allowedMimeTypes.contains(contentType)) {
throw new SecurityException(
"허용되지 않은 파일 타입");
}
// 3. 파일 매직 넘버 검증 (실제 내용)
byte[] fileBytes = file.getBytes();
if (!isValidFileSignature(fileBytes, extension)) {
throw new SecurityException(
"파일 내용이 확장자와 일치하지 않음");
}
// 4. 파일 크기 제한 (10MB)
if (file.getSize() > 10 * 1024 * 1024) {
throw new IllegalArgumentException(
"파일 크기 초과 (최대 10MB)");
}
// 5. 안전한 파일명 생성 (UUID)
String safeFileName = UUID.randomUUID().toString()
+ "." + extension;
// 6. 실행 불가능한 디렉토리에 저장
String uploadPath = "/data/uploads/" + safeFileName;
Path path = Paths.get(uploadPath);
Files.write(path, fileBytes);
// 7. DB에 메타데이터 저장
FileMetadata metadata = new FileMetadata();
metadata.setOriginalName(fileName);
metadata.setSafeName(safeFileName);
metadata.setUploadTime(LocalDateTime.now());
fileRepository.save(metadata);
return "success";
}
// 파일 매직 넘버 검증
private boolean isValidFileSignature(
byte[] bytes, String extension) {
if (bytes.length < 4) return false;
// JPEG: FF D8 FF
if (extension.equals("jpg") || extension.equals("jpeg")) {
return bytes[0] == (byte)0xFF &&
bytes[1] == (byte)0xD8 &&
bytes[2] == (byte)0xFF;
}
// PNG: 89 50 4E 47
if (extension.equals("png")) {
return bytes[0] == (byte)0x89 &&
bytes[1] == (byte)0x50 &&
bytes[2] == (byte)0x4E &&
bytes[3] == (byte)0x47;
}
// PDF: 25 50 44 46
if (extension.equals("pdf")) {
return bytes[0] == (byte)0x25 &&
bytes[1] == (byte)0x50 &&
bytes[2] == (byte)0x44 &&
bytes[3] == (byte)0x46;
}
return true;
}