잘못된 디렉터리 경로 제한 ('경로 탐색')

허용된 디렉터리 밖의 파일 접근을 막는 경로 검증

설명

경로 탐색(Path Traversal) 취약점은 애플리케이션이 사용자 입력을 파일 경로로 사용하면서 허용된 디렉터리 경계를 확인하지 않을 때 발생합니다. 공격자는 상대 경로(../ 또는 ..\\)를 이용하여 시스템의 민감한 파일에 접근할 수 있습니다.

잠재적 영향

영향은 애플리케이션 계정의 파일 권한과 읽기·쓰기 기능에 따라 달라집니다.

  • 민감한 파일 노출: 시스템 파일이나 다른 사용자의 데이터에서 중요 정보를 읽을 수 있습니다.
  • 권한 우회: 다른 사용자의 데이터나 설정 파일을 무단으로 읽거나 수정할 수 있습니다.
  • 원격 코드 실행: 애플리케이션이 읽은 파일을 실행하는 경우 악성 코드 실행이 가능할 수 있습니다.

해결 방법

  1. 사용자 입력값을 직접 파일 경로로 사용하지 않기
    • 사용자 입력을 검증하고, 허용된 디렉터리 내에서만 파일을 접근할 수 있도록 제한합니다.
  2. 파일 경로 검증 수행하기
    • Path.resolve()로 경로를 결합한 뒤 정규화하고 허용된 기준 경로와 비교하세요. resolve()만으로 디렉터리 경계를 확인할 수는 없습니다.
    • 정규화된 Path를 구성 요소 단위로 비교하고, 실제 파일을 읽을 때는 toRealPath()로 심볼릭 링크까지 해석한 경로가 허용 디렉터리 안에 있는지 다시 확인합니다.
    • 공격자가 기준 디렉터리를 변경할 수 있다면 검사와 사용 사이의 심볼릭 링크 교체를 막기 위해 디렉터리 권한을 제한하고 SecureDirectoryStream 같은 디렉터리 상대 API를 고려합니다.
  3. 파일 이름을 안전한 값으로 변환하여 사용하기
    • UUID 또는 해시 기반의 안전한 파일명을 생성하여 사용합니다.

예시

변경 전

java
private static final String BASE_PATH ="/restricted/directory";

public String readFile(String filePath) throws IOException {
    Path file = Paths.get("/restricted/directory/" + filePath);
    return new String(Files.readAllBytes(file));
}

File(parent, child) 생성자의 두 번째 인자도 경로의 일부입니다. 기준 디렉터리가 고정되어 있어도 신뢰할 수 없는 child에 .. 경로 구성 요소가 포함되면 정규화 과정에서 기준 디렉터리를 벗어날 수 있습니다.

java
import java.io.File;
import javax.servlet.http.HttpServletRequest;

public final class UnsafeUploadPath {
    private static final File UPLOAD_DIR = new File("/var/app/uploads");

    public File destination(HttpServletRequest request) {
        String child = request.getParameter("name");

        // 취약: 두 번째 생성자 인자도 공격자가 제어하는 경로입니다.
        return new File(UPLOAD_DIR, child);
    }
}

변경 후

다음 Java 11 이상 예시는 공격자가 기준 디렉터리와 그 내부 경로를 바꿀 수 없는 환경을 전제로 합니다. 파일 크기 제한과 잘못된 입력에 대한 오류 응답은 별도로 적용하세요.

java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;

public final class SafeFileReader {
    private static final Path BASE_PATH = Path.of("/restricted/directory");

    public String readFile(String filePath) throws IOException {
        Path base = BASE_PATH.toRealPath();
        Path supplied = Path.of(filePath);
        if (supplied.isAbsolute()) {
            throw new SecurityException("Absolute paths are not allowed.");
        }

        Path candidate = base.resolve(supplied).normalize();
        if (!candidate.startsWith(base)) {
            throw new SecurityException("Unauthorized access attempt detected.");
        }

        // 기존 파일의 심볼릭 링크까지 해석한 실제 대상도 기준 디렉터리 안이어야 합니다.
        Path requestedPath = candidate.toRealPath();
        if (!requestedPath.startsWith(base)
                || !Files.isRegularFile(requestedPath, LinkOption.NOFOLLOW_LINKS)) {
            throw new SecurityException("Unauthorized access attempt detected.");
        }
        return Files.readString(requestedPath, StandardCharsets.UTF_8);
    }
}

설명:

  • 변경 전: 신뢰할 수 없는 사용자 입력을 경로로 직접 사용하는 것은 위험합니다. 공격자는 ../를 사용하여 상위 디렉터리로 나가 제한된 영역 밖의 파일에 접근할 수 있습니다. 이 위험은 new File(parent, child)의 child 인자에도 동일하게 적용됩니다.
  • 변경 후: 절대 경로를 거부하고 Path.startsWith(Path)로 정규화된 경로 구성 요소를 비교합니다. 이어서 toRealPath()로 심볼릭 링크를 해석한 실제 대상도 정규화된 기준 디렉터리 안에 있는지 확인한 뒤, 검증한 requestedPath 자체를 읽습니다.

관련 CVE

참조