[Java] Directory Traversal 방어 코드

식빵·2024년 4월 29일
0

Java Lab

목록 보기
23/29
post-thumbnail
post-custom-banner
import java.io.File;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * <h2>[보안] 경로 순회(directory traversal) 방어 유틸 클래스</h2>
 * 경로 순회 문자열을 제거하며 더불어서 루트 경로, 웹루트에 대해서는 예외를 던지는 보안성 유틸 클래스이다.
 */
public class FileDirectoryTraversalDefender {

    /**
     * <h3>경로 순회 문자열 제거 메소드</h3>
     * 참고: return 값의 파일 경로 구분자는 항상 "/" 가 적용된다.
     * @see FileDirectoryTraversalDefender#cleanPath(String, boolean)
     */
    public static String cleanPath(String filePath) throws IllegalArgumentException {
        // Java 는 파일 경로 구분자가 "\" 든 "/" 든 똑같이 대응하도록 되어 있다.
        // 그러므로 useOsFileSeparatorOnResult=false 로 세팅한다.
        return cleanPath(filePath, false);
    }

    /**
     * <h3>경로 순회 문자열 제거 메소드</h3>
     * @param filePath 경로 순회 문자열을 제거하고자 하는 파일경로 문자열
     * @param useOsFileSeparatorOnResult 최종 return 문자열의 파일 경로 구분자를 OS 기반에 맞게 변경할지 여부이며 true 면 적용된다.
     *  true 설정하면 window 의 경우 "C:\sdf\some.txt" 처럼 반환, false 면 "C:/sdf/some.txt" 로 반환
     * @return 경로 순회 문자열이 없는 문자열
     * @throws IllegalArgumentException 경로 순회 문자열 제거 후, 루트 경로 혹은 WEB Root 와 관련된 문자열이 발견되면 예외를 던진다.
     */
    public static String cleanPath(String filePath, boolean useOsFileSeparatorOnResult) throws IllegalArgumentException {
        if(filePath == null || filePath.trim().isEmpty()) {
            return filePath;
        }
        
        filePath = filePath.trim();

        /*
        윈도우, 리눅스 등 어떤 운영체제를 사용하던 간에 똑같이 경로 구분자를 "/" 로 통일한다.
        이는 똑같은 로직을 태우기 위함이다.
        만약에 최종 경로 String 값의 경로구분자가 OS 기반에 맞게 하고 싶다면 [useOsFileSeparatorOnResult=true] 로 설정한다.
        */
        String sanitizedPath = filePath.replaceAll("\\\\+", "/");

        // "../" 경로 제거
        sanitizedPath = sanitizedPath.replaceAll("\\.\\.", "");

        // "/./" 경로 제거
        sanitizedPath = sanitizedPath.replaceAll("/\\./", "/");

        // "&" 제거
        sanitizedPath = sanitizedPath.replaceAll("&", "");

        // Replace multiple consecutive slashes with a single slash
        sanitizedPath = sanitizedPath.replaceAll("/{2,}", "/");

        // 루트 경로 사용 검사 ("/" or "C:/"). 발견되면 예외를 던진다.
        checkRootPathUsage(filePath, sanitizedPath);

        // URL 경로 체크(= Web root 사용여부 검사). 발견되면 예외를 던진다.
        checkUrlLikePathUsage(filePath, sanitizedPath);

        // 최종적으로 운영체제에 맞게 문자열을 반환할지 분기처리하여 return 한다.
        return useOsFileSeparatorOnResult ? 
        		changeFileSeparatorDependOnOs(sanitizedPath) : sanitizedPath;
    }

    /**
     * 루트 경로 사용 여부를 체크한다.
     */
    private static void checkRootPathUsage(String filePath, String sanitizedPath) {
        if (sanitizedPath.equals("/") || sanitizedPath.matches("[A-Za-z]:/$")) {
            throw new IllegalArgumentException("Invalid path: " + filePath);
        }
    }

    /**
     * URL Path 사용 여부를 체크한다. Web Root 접근 방어용이다.
     */
    private static void checkUrlLikePathUsage(String filePath, String sanitizedPath) throws IllegalArgumentException {
        Path path = Paths.get(sanitizedPath);
        // Check if the path is a URL-like path (potentially a web root)
        URI uri = path.toUri();
        if (uri.getScheme() != null && uri.getHost() != null) {
            throw new IllegalArgumentException("Path resembles a URL-like path. Error Occurred Path => " + filePath);
        }
    }

    /**
     * 파일 경로 구분자를 운영체제에 맞게 변경한다.
     */
    public static String changeFileSeparatorDependOnOs(String path) {
        String osName = System.getProperty("os.name").toLowerCase();
        return osName.toLowerCase().contains("win") ? path.replace("/", File.separator) : path;
    }

    /**
     * File 인스턴스의 경로에서 경로 순회 문자열을 제거하고 다시 File 을 생성한다.
     */
    public static File cleanPath(File file) {
        String path = file.getPath();
        return new File(cleanPath(path));
    }

    /**
     * Path 인스턴스의 경로에서 경로 순회 문자열을 제거하고 다시 File 을 생성한다.
     */
    public static Path cleanPath(Path file) {
        String path = file.toFile().getPath();
        return Paths.get(cleanPath(path));
    }


    /**
     * 테스트 코드
     */
    public static void main(String[] args) {
        String filePath1 = "../../myfile.txt";
        String filePath2 = "C:\\platform\\..\\user1\\myfile.txt";
        String filePath3 = "C:\\11";

        String sanitizedPath1 = cleanPath(filePath1);
        String sanitizedPath2 = cleanPath(filePath3);
        String sanitizedPath3 = cleanPath(filePath4);
        
        System.out.println("Sanitized Path 1: " + sanitizedPath1);
        System.out.println("Sanitized Path 2: " + sanitizedPath2);
        System.out.println("Sanitized Path 3: " + sanitizedPath3);
    }
}
profile
백엔드를 계속 배우고 있는 개발자입니다 😊
post-custom-banner

0개의 댓글