빌드노트
Path.resolve에 절대 경로를 넣으면 기준 경로가 사라진다
Path.resolve는 절대 입력을 받으면 기준 경로 대신 입력을 반환한다. Windows 11·OpenJDK 17.0.2에서 상대·절대·루트 입력을 비교하고, 상대 경로만 허용하는 검사와 normalize의 한계를 설명한다.
base.resolve(input)을 호출했는데 결과에서 base가 사라졌다면, 먼저 input.isAbsolute()를 확인한다. 입력이 절대 경로이면 resolve는 기준 경로를 붙이지 않고 입력 경로를 그대로 반환한다. 기준 디렉터리 아래의 경로만 받으려면, 호출 전에 허용할 입력 형식을 정해야 한다.
이는 Java 17 Path.resolve 문서 (새 탭에서 열림)에 명시된 동작이다. 문자열을 넘기는 resolve(String)도 같은 규칙을 적용한다. 뒤에 normalize()를 붙여도 절대 입력이 기준 경로 아래로 바뀌지는 않는다.
같은 base에 상대 경로와 절대 경로를 넣어 보면
다음 코드는 입력의 절대 여부와 resolve 결과를 나란히 보여 준다. JDK가 설치된 Windows에서 내용을 ResolveDemo.java로 저장한다. 경로 이름은 비교를 위한 가상 값이며, 코드가 해당 위치의 파일을 열거나 디렉터리를 만들지는 않는다.
import java.nio.file.Path;
public class ResolveDemo {
public static void main(String[] args) {
Path base = Path.of("C:/resolve-demo/base");
for (String raw : new String[]{"report.txt", "C:/resolve-demo/outside.txt"}) {
Path input = base.getFileSystem().getPath(raw);
System.out.printf("input=%s, absolute=%s, resolved=%s%n",
input, input.isAbsolute(), base.resolve(input));
}
}
}
파일을 저장한 폴더에서 java ResolveDemo.java를 실행한다. Windows 11, OpenJDK 17.0.2의 기본 파일 시스템으로 실행한 출력은 다음과 같다.
input=report.txt, absolute=false, resolved=C:\resolve-demo\base\report.txt
input=C:\resolve-demo\outside.txt, absolute=true, resolved=C:\resolve-demo\outside.txt
첫 입력에는 base가 붙었다. 두 번째 입력은 드라이브와 루트가 있는 절대 경로여서 결과가 입력과 같다. 이 경우에는 기준 경로의 값부터 바꿀 것이 아니라, 호출부가 절대 경로도 허용하려던 것인지 확인해야 한다. 절대 경로를 직접 지정하는 설정이라면 이 동작을 사용할 수 있다. 기준 디렉터리에 붙일 이름을 받는 입력이라면 받아들이기 전에 거부해야 한다.
출력이 다르다면 실행 OS와 입력의 isAbsolute()부터 비교한다. 위의 C:/...는 Windows용 입력이다. Linux나 macOS에서 같은 문자열이 절대 경로가 된다고 가정하면 안 된다. 이 글에서는 두 OS의 JVM을 실행하지 않았다.
상대 입력만 받는다는 계약을 코드에 넣기
Windows에서는 isAbsolute()가 false인 것만으로 충분하지 않았다. 같은 기준 경로에 /outside.txt를 넣자 isAbsolute()는 false, getRoot()는 \, 결과는 C:\outside.txt였다. C:report.txt 역시 false였지만 루트 성분은 C:였다. 이 실행에서는 base 아래에 붙었어도, 드라이브를 명시한 입력까지 일반적인 상대 이름으로 허용할지는 별도로 정해야 한다. 공식 문서도 절대 경로가 아닌 입력에 루트 성분이 있는 경우의 해석은 구현에 의존한다고 설명한다.
아래 메서드는 루트 성분이 없는 상대 입력만 받고, 정규화한 결과가 base의 경로 요소로 시작해야 한다는 계약을 구현한다. 앞 예제처럼 Path를 import한 클래스 안에 두고 사용한다. base는 애플리케이션에서 정한 절대 경로여야 하며, 이 검사는 실제 파일 접근의 안전성을 보장하지 않는다.
static Path resolveRelative(Path base, String raw) {
Path input = base.getFileSystem().getPath(raw);
if (input.isAbsolute() || input.getRoot() != null) {
throw new IllegalArgumentException("rooted input");
}
Path parent = base.normalize();
Path child = parent.resolve(input).normalize();
if (!child.startsWith(parent)) {
throw new IllegalArgumentException("outside base");
}
return child;
}
같은 base로 이 메서드를 실행했을 때 report.txt와 sub/../report.txt는 모두 C:\resolve-demo\base\report.txt를 반환했다. 절대 입력, /outside.txt, C:report.txt는 rooted input으로 거부했다. 루트가 없는 ../outside.txt도 정규화 후에는 C:\resolve-demo\outside.txt가 되므로 outside base로 거부했다.
여기서 startsWith는 문자열로 변환하지 않고 Path끼리 비교한다. 함께 실행한 ../base-copy/report.txt도 거부됐다. base-copy라는 이름이 base라는 글자로 시작하는 것과, base 디렉터리 아래의 경로인 것은 다르다. 빈 입력이나 .까지 금지하려는 계약이라면 base 자체를 반환하는 경우도 따로 거부해야 한다.
normalize는 실제 파일의 위치를 확인하지 않는다
위 검사는 경로 표현을 정리한 뒤 포함 관계를 비교한다. Path.normalize 문서 (새 탭에서 열림)는 이 메서드가 파일 시스템에 접근하지 않으며, 심볼릭 링크가 있으면 ..를 제거한 경로가 원래와 다른 파일을 가리킬 수도 있다고 설명한다.
따라서 이 메서드를 통과했다는 사실을 “실제로 여는 파일도 반드시 base 아래에 있다”는 보장으로 사용하면 안 된다. 이번 실행은 합성 Path 객체의 해석과 입력 거부만 확인했다. 파일 존재 여부, 심볼릭 링크·Windows junction, 검사 뒤 경로가 바뀌는 상황과 실제 파일 열기는 검증하지 않았다. 외부 입력으로 파일을 여는 기능이라면 그 접근 단계의 검증이 별도로 필요하다.