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

파일 다운로드 (경로 탐색) | 위험도: 5 (매우 높음) | 통제구분: 5.8.4 서비스 보호

OO은행

인터넷뱅킹 - 거래내역 다운로드 서비스

취약점: 파일 경로 검증 미흡

거래내역 다운로드

계좌별 거래내역을 엑셀 파일로 다운로드합니다

이용 안내

거래내역 파일은 /user_data/documents/ 폴더에 저장됩니다

경로 탐색 공격 시나리오

취약한 Java 코드

위험
// ❌ 취약한 코드 - 경로 검증 없음
@GetMapping("/download")
public ResponseEntity<Resource> downloadFile(
    @RequestParam String fileName) {
    
    // 사용자 입력을 그대로 경로에 결합!
    String filePath = "/user_data/documents/" + fileName;
    
    // 경로 검증 없이 파일 읽기
    File file = new File(filePath);
    Resource resource = new FileSystemResource(file);
    
    return ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION,
                "attachment; filename=\"" + fileName + "\"")
        .body(resource);
}

서버 응답

READY
[SERVER] Ready for file download request...

안전한 Java 코드

권장
// ✅ 안전한 코드 - 경로 검증 및 정규화
@GetMapping("/download")
public ResponseEntity<Resource> downloadFile(
    @RequestParam String fileName) throws Exception {
    
    // 1. 경로 탐색 문자 제거
    fileName = fileName.replaceAll("\\.\\.", "")
                       .replaceAll("/", "")
                       .replaceAll("\\\\", "");
    
    // 2. 파일명만 추출 (경로 제거)
    Path path = Paths.get(fileName);
    fileName = path.getFileName().toString();
    
    // 3. 허용된 확장자만 허용
    String extension = fileName.substring(
        fileName.lastIndexOf(".") + 1
    ).toLowerCase();
    
    Set<String> allowedExtensions = Set.of(
        "xlsx", "pdf", "csv"
    );
    
    if (!allowedExtensions.contains(extension)) {
        throw new SecurityException(
            "허용되지 않은 파일 형식"
        );
    }
    
    // 4. 베이스 디렉토리 설정
    String baseDir = "/user_data/documents/";
    Path basePath = Paths.get(baseDir).normalize();
    
    // 5. 전체 경로 생성 및 정규화
    Path fullPath = basePath.resolve(fileName).normalize();
    
    // 6. 경로 벗어남 체크
    if (!fullPath.startsWith(basePath)) {
        throw new SecurityException(
            "허용된 경로 외부 접근 시도"
        );
    }
    
    // 7. 파일 존재 여부 확인
    File file = fullPath.toFile();
    if (!file.exists() || !file.isFile()) {
        throw new FileNotFoundException(
            "파일을 찾을 수 없습니다"
        );
    }
    
    // 8. 안전하게 다운로드
    Resource resource = new FileSystemResource(file);
    
    return ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION,
                "attachment; filename=\"" + 
                URLEncoder.encode(fileName, "UTF-8") + "\"")
        .body(resource);
}

// 추가: 화이트리스트 기반 파일 검증
private boolean isAllowedFile(String fileName) {
    // DB에 저장된 허용 파일 목록 확인
    List<String> allowedFiles = 
        fileRepository.findAllowedFiles();
    
    return allowedFiles.contains(fileName);
}

경로 탐색 방어 체크리스트

경로 문자 제거: ../ \\ / 제거
파일명만 추출: basename() 사용
경로 정규화: normalize() 적용
베이스 경로 검증: startsWith() 확인
화이트리스트: 허용된 파일만 다운로드