설명
파일 업로드 기능에서 사용자 입력을 기반으로 파일명을 저장할 경우, 공격자가 조작된 파일명을 제출하여 원하지 않는 위치에 파일을 저장하거나 악성 파일을 실행할 가능성이 있습니다.
특히, ../ 또는 ..\ 등의 경로 탐색(Path Traversal) 문자를 포함한 파일명이 허용되면, 애플리케이션이 의도하지 않은 경로에 파일을 저장하게 되어 보안 위험이 발생합니다.
잠재적 영향
- 임의 파일 저장: 공격자가 파일명을 조작하여 애플리케이션이 제한된 디렉터리가 아닌 임의의 위치에 파일을 저장하게 만들 수 있습니다.
- 파일 덮어쓰기: 쓰기 API가 덮어쓰기를 허용하고 프로세스에 권한이 있으면 기존 파일을 손상시킬 수 있습니다.
- 코드 실행: 업로드한 파일을 서버가 실행하도록 배포된 경우 코드 실행으로 이어질 수 있습니다. 파일이 저장된다는 사실만으로 실행되지는 않습니다.
해결 방법
-
파일명을 직접 사용하지 않고 서버에서 생성된 안전한 파일명을 사용할 것
- UUID 또는 안전한 해시값을 기반으로 파일명을 생성하여 사용합니다.
-
파일명 검증 및 필터링 수행
- 경로 문자를 단순 삭제하는 대신 허용된 파일명 형식만 받아들이고, 빈 이름이나
.·..같은 특수 이름은 거부하세요. - 정규 표현식을 사용하여 안전한 파일명 패턴(
^[a-zA-Z0-9._-]+$)을 정의하고, 이를 벗어나는 입력을 차단합니다.
- 경로 문자를 단순 삭제하는 대신 허용된 파일명 형식만 받아들이고, 빈 이름이나
-
파일 저장 경로 제한
- 특정한 업로드 디렉터리(
/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 기반의 안전한 파일명을 생성하여 사용합니다.