파일 업로드 파일명 검증 취약점

File Upload Lack of File Name Validation

설명

파일 업로드 기능에서 사용자 입력을 기반으로 파일명을 저장할 경우, 공격자가 조작된 파일명을 제출하여 원하지 않는 위치에 파일을 저장하거나 악성 파일을 실행할 가능성이 있습니다.
특히, ../ 또는 ..\ 등의 경로 탐색(Path Traversal) 문자를 포함한 파일명이 허용되면, 애플리케이션이 의도하지 않은 경로에 파일을 저장하게 되어 보안 위험이 발생합니다.

잠재적 영향

  • 임의 파일 저장: 공격자가 파일명을 조작하여 애플리케이션이 제한된 디렉터리가 아닌 임의의 위치에 파일을 저장하게 만들 수 있습니다.
  • 파일 덮어쓰기: 쓰기 API가 덮어쓰기를 허용하고 프로세스에 권한이 있으면 기존 파일을 손상시킬 수 있습니다.
  • 코드 실행: 업로드한 파일을 서버가 실행하도록 배포된 경우 코드 실행으로 이어질 수 있습니다. 파일이 저장된다는 사실만으로 실행되지는 않습니다.

해결 방법

  1. 파일명을 직접 사용하지 않고 서버에서 생성된 안전한 파일명을 사용할 것

    • UUID 또는 안전한 해시값을 기반으로 파일명을 생성하여 사용합니다.
  2. 파일명 검증 및 필터링 수행

    • 경로 문자를 단순 삭제하는 대신 허용된 파일명 형식만 받아들이고, 빈 이름이나 .·.. 같은 특수 이름은 거부하세요.
    • 정규 표현식을 사용하여 안전한 파일명 패턴(^[a-zA-Z0-9._-]+$)을 정의하고, 이를 벗어나는 입력을 차단합니다.
  3. 파일 저장 경로 제한

    • 특정한 업로드 디렉터리(/var/app/uploads 등) 내에서만 파일을 저장하고, 해당 디렉터리를 벗어나지 않도록 검증합니다.

예시

javax.servlet 기반의 업로드 처리 발췌입니다. multipart 설정, 인증·인가, CSRF 방어, 크기·내용 검사와 오류 처리는 별도로 준비하세요. uploads는 미리 만든 서버 관리 디렉터리이며, 웹 실행 경로 밖에 두고 다른 사용자의 변경을 막아야 합니다. .txt 확장자는 내용 검증을 대신하지 않습니다.

변경 전

java
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;

@WebServlet("/upload")
public class FileUploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Part filePart = request.getPart("file");
String fileName = filePart.getSubmittedFileName(); // 사용자 입력값 직접 사용
Files.copy(filePart.getInputStream(), Paths.get("uploads", fileName));
}
}

변경 후

java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;

@WebServlet("/upload")
public class SecureFileUploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Part filePart = request.getPart("file");
String fileName = filePart.getSubmittedFileName();

        if (fileName == null || fileName.equals(".") || fileName.equals("..") || !isFileNameAllowed(fileName)) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid file name");
            return;
        }

        // 서버에서 안전한 파일명 생성
        String storedFileName = UUID.randomUUID().toString() + ".txt";
        Path targetPath = Paths.get("uploads").resolve(storedFileName);
        Files.copy(filePart.getInputStream(), targetPath);
    }

    private boolean isFileNameAllowed(String fileName) {
        return fileName.matches("^[a-zA-Z0-9._-]+$"); // 안전한 파일명 패턴 검증
    }
}

설명:

  • 변경 전:
    • 사용자 파일명이 저장 경로에 들어가므로, 쓰기 권한이 있는 의도하지 않은 위치에 새 파일을 만들 수 있습니다. 이 Files.copy() 호출은 기존 대상이 있으면 기본적으로 실패하며, 파일을 읽는 예시가 아닙니다.
  • 변경 후:
    • 제출된 이름이 비어 있거나 특수 이름이면 거부하고 허용된 패턴을 검사합니다. 플랫폼별 경로 구분자 제거에 의존하지 않습니다.
    • 서버에서 UUID 기반의 안전한 파일명을 생성하여 사용합니다.

참조