전자금융기반시설 보안 취약점 평가기준 - WEB-SER-002

악성파일 업로드 | 위험도: 5 (매우 높음) | 통제구분: 5.8.4 서비스 보호

OO은행

인터넷뱅킹 - 금융상담 신청 시스템

취약점: 파일 확장자 검증 미흡

금융상담 신청서 제출

재직증명서, 소득증빙서류 등을 첨부해주세요

업로드할 파일을 선택하세요

웹쉘 (Web Shell) 이란?

업로드 취약점을 통해 해커가 원격에서 웹 서버를 조종할 수 있도록 작성한 웹 스크립트

// 웹쉘 예시 (JSP)
<% Runtime.getRuntime()
  .exec(request.getParameter("cmd")); %>

⚠️ 업로드 후 실행 시 시스템 명령어 실행 가능

취약한 Java 코드

위험
// ❌ 취약한 코드 - 파일 검증 없음
@PostMapping("/upload")
public String uploadFile(
    @RequestParam("file") MultipartFile file) {
    
    // 파일명만 받아서 저장! (위험)
    String fileName = file.getOriginalFilename();
    String uploadPath = "/uploads/" + fileName;
    
    // 확장자 검증 없이 저장
    file.transferTo(new File(uploadPath));
    
    return "success";
}

서버 로그

READY
[SYSTEM] Server ready. Waiting for upload...

안전한 Java 코드

권장
// ✅ 안전한 코드 - 다층 검증
@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;
}

파일 업로드 방어 체크리스트

확장자 화이트리스트: jpg, png, pdf만 허용
MIME 타입 검증: Content-Type 확인
매직 넘버 검증: 파일 실제 내용 확인
파일명 변경: UUID로 안전한 이름 생성
실행 권한 제거: 업로드 디렉토리 실행 불가