경로 조작 취약점이란? 디렉터리 트래버설의 원리와 대응 방법

경로 조작 취약점과 디렉터리 트래버설이 발생하는 원리를 알아보고, 안전한 파일 경로 검증과 기준 디렉터리 제한을 이용한 대응 방법을 살펴봅니다.

웹사이트에서 첨부파일을 내려받을 때 주소를 살펴보면 파일명이나 파일 번호가 파라미터로 전달되는 경우가 있습니다. 서버는 해당 값을 받아 저장된 파일의 위치를 찾고 사용자에게 파일을 전송합니다.

정상적인 파일만 선택할 수 있다면 별다른 문제가 없습니다. 하지만 사용자가 전달한 파일명이나 경로를 검증하지 않고 서버의 디렉터리와 직접 연결한다면 이야기가 달라집니다. 공격자가 상위 디렉터리로 이동하는 경로를 입력해 공개할 의도가 없었던 파일에 접근할 수 있기 때문입니다.

이러한 보안 문제를 경로 조작 취약점(Path Manipulation) 또는 디렉터리 트래버설(Directory Traversal)이라고 합니다.

경로 조작 취약점이란?

경로 조작 취약점은 외부 입력값이 파일이나 디렉터리 경로를 결정하는 데 사용될 때 발생합니다. 애플리케이션이 입력값을 충분히 검증하지 않으면 사용자가 원래 허용된 디렉터리를 벗어나 다른 위치의 파일에 접근할 수 있습니다.

디렉터리 트래버설은 경로 조작 취약점에서 자주 사용되는 방식입니다. ../와 같이 상위 디렉터리로 이동하는 상대경로 표현을 이용해 지정된 저장공간 밖으로 이동합니다.

예를 들어 서버가 다음 경로에 다운로드 파일을 저장한다고 가정해 보겠습니다.

/var/app/downloads/

사용자가 guide.pdf를 요청하면 서버는 두 값을 연결해 다음 파일을 찾을 수 있습니다.

/var/app/downloads/guide.pdf

그런데 서버가 파일명을 그대로 신뢰한다면 상위 경로를 가리키는 값이 전달됐을 때 최종 접근 위치가 다운로드 디렉터리 밖으로 벗어날 수 있습니다.

경로 조작 취약점의 핵심은 단순합니다.

사용자 입력값이 서버의 실제 파일 경로를 직접 결정하게 두는 것입니다.

경로 조작과 디렉터리 트래버설의 차이

두 용어는 비슷한 의미로 사용되지만 범위를 구분하면 이해하기 쉽습니다.

경로 조작은 외부 입력값으로 파일 경로나 디렉터리 위치를 조작할 수 있는 문제 전체를 의미합니다. 파일 읽기뿐만 아니라 저장, 삭제, 복사, 압축 해제 과정에서도 발생할 수 있습니다.

디렉터리 트래버설은 상대경로나 인코딩된 경로 등을 이용해 허용된 기준 디렉터리를 벗어나는 구체적인 방법을 가리킵니다.

즉, 경로 조작이 더 넓은 개념이고 디렉터리 트래버설은 그 안에서 자주 나타나는 공격 방식이라고 볼 수 있습니다.

취약한 파일 다운로드 코드

다음은 자바로 작성된 간단한 파일 다운로드 기능의 일부입니다.

String fileName = request.getParameter("file");

Path filePath = Paths.get(
    "/var/app/downloads/" + fileName
);

byte[] data = Files.readAllBytes(filePath);

코드의 목적은 /var/app/downloads/ 디렉터리에서 사용자가 요청한 파일을 읽는 것입니다. 하지만 fileName은 HTTP 요청에서 전달되는 외부 입력값이며 별도의 검증 없이 경로 문자열에 연결됩니다.

이 코드의 데이터 흐름을 따라가면 문제를 쉽게 확인할 수 있습니다.

HTTP 파라미터
→ fileName 변수
→ 기준 경로와 문자열 결합
→ 파일 읽기

사용자 입력값이 파일의 실제 위치를 결정하고 있는데도 서버는 최종 경로가 다운로드 디렉터리 내부인지 확인하지 않습니다. 이처럼 외부 입력값이 File, FileInputStream, Paths.get() 또는 Files.readAllBytes()와 같은 파일 처리 기능으로 전달될 때는 경로 조작 여부를 살펴봐야 합니다.

어떤 기능에서 주로 발견될까?

경로 조작 취약점은 파일 다운로드 기능에서 가장 쉽게 떠올릴 수 있지만, 실제로는 다양한 기능에서 발견됩니다.

  • 게시판 첨부파일 다운로드
  • 프로필 이미지 조회
  • 로그 파일 확인
  • 문서 미리보기
  • 이미지 크기 변환
  • 파일 백업과 복원
  • 템플릿 또는 도움말 파일 불러오기
  • 압축파일 해제
  • 사용자별 파일 저장
  • 관리자 페이지의 설정파일 조회
  • 언어 또는 테마 파일 선택

특히 관리자용 기능이라고 해서 안전하다고 볼 수는 없습니다. 접근 권한이 있는 계정이 탈취되거나 권한 검증이 부족하면 경로 조작 취약점과 결합해 더 큰 피해가 발생할 수 있습니다.

파일 업로드 기능도 확인해야 합니다. 사용자가 전송한 파일명을 서버의 저장 경로에 그대로 연결하면 의도하지 않은 위치에 파일이 저장되거나 기존 파일이 덮어써질 가능성이 있습니다.

경로 조작으로 발생할 수 있는 피해

경로 조작 취약점의 영향은 애플리케이션이 파일을 읽는지, 저장하는지, 삭제하는지에 따라 달라집니다.

파일 읽기 기능이 취약하다면 서버 설정파일, 애플리케이션 소스 코드, 로그, 인증정보 등이 노출될 수 있습니다. 환경설정 파일에는 데이터베이스 계정이나 외부 API 키가 포함되는 경우도 있어 다른 시스템의 침해로 이어질 가능성이 있습니다.

파일 쓰기 기능에서 발생하면 피해가 더 커질 수 있습니다. 공격자가 저장 경로를 조작해 기존 파일을 덮어쓰거나 웹에서 실행 가능한 위치에 파일을 저장할 수 있기 때문입니다.

대표적인 피해는 다음과 같습니다.

  • 서버 내부 설정파일 열람
  • 소스 코드와 로그파일 노출
  • 데이터베이스 접속정보 유출
  • 다른 사용자의 첨부파일 접근
  • 중요 파일 덮어쓰기
  • 파일 삭제 및 서비스 장애
  • 악성 파일이 실행 가능한 위치에 저장
  • 백업파일과 임시파일 노출

다만 실제 접근 범위는 웹 애플리케이션이 실행되는 계정의 권한에 따라 달라집니다. 서비스 계정이 읽을 수 없는 파일까지 자동으로 접근할 수 있는 것은 아닙니다. 이 때문에 안전한 경로 검증과 함께 운영체제의 파일 권한을 최소화하는 조치가 필요합니다.

문자열에서 ../만 제거하면 안전할까?

경로 조작 취약점에 대응하면서 자주 하는 실수가 ../ 문자열만 제거하는 것입니다.

fileName = fileName.replace("../", "");

겉으로 보면 상위 디렉터리 이동을 막은 것처럼 보입니다. 하지만 운영체제마다 경로 구분자가 다르고, 중첩된 문자열이나 URL 인코딩 처리 순서에 따라 예상하지 못한 값이 만들어질 수 있습니다.

애플리케이션 앞단과 뒷단에서 디코딩이 여러 번 수행되는 경우도 있습니다. 처음 검사할 때는 안전해 보였던 값이 나중에 다시 디코딩되면서 경로 문법으로 바뀔 수 있습니다.

다음과 같은 차단 방식도 단독으로 사용해서는 안 됩니다.

  • 점 문자를 모두 제거하는 방법
  • 슬래시만 제거하는 방법
  • 특정 운영체제의 경로 구분자만 검사하는 방법
  • 파일 확장자만 확인하는 방법
  • 입력값에 특정 문자열이 포함됐는지만 검사하는 방법

차단 목록은 예상한 패턴만 막을 수 있습니다. 경로 검증에서는 허용할 값의 범위를 정하고 최종 경로가 기준 디렉터리 안에 있는지 확인하는 것이 더 중요합니다.

가장 안전한 방법은 파일 식별자를 사용하는 것

파일 다운로드 주소에 서버의 실제 파일명을 노출할 필요는 없습니다. 사용자가 파일 번호나 임의의 식별자를 전달하면 서버가 데이터베이스에서 실제 저장 경로를 조회하도록 구성할 수 있습니다.

예를 들어 사용자는 다음과 같은 주소로 파일을 요청합니다.

/download?fileId=1842

서버는 fileId가 현재 사용자에게 허용된 파일인지 확인한 뒤 데이터베이스에서 실제 저장 이름을 가져옵니다.

사용자 요청 fileId
→ 숫자 형식 확인
→ 파일 소유자 및 접근권한 확인
→ 데이터베이스에서 저장 경로 조회
→ 파일 전송

이 구조에서는 사용자가 실제 파일 경로를 직접 지정하지 않습니다. 서버가 미리 저장해 둔 값으로 경로를 결정하므로 경로 조작 가능성을 크게 줄일 수 있습니다.

파일을 저장할 때도 원본 파일명을 그대로 사용하기보다 서버에서 생성한 임의의 이름으로 보관하는 것이 좋습니다. 화면에 표시할 원본 파일명과 서버에 저장할 실제 파일명을 분리하면 보안과 파일 관리 측면에서 모두 유리합니다.

Path.normalize를 이용한 경로 검증

사용자로부터 파일명이나 하위 경로를 받아야 한다면 최종 경로를 정규화한 뒤 기준 디렉터리 내부에 포함되는지 검사해야 합니다.

Path baseDir = Paths.get(
    "/var/app/downloads"
).toAbsolutePath().normalize();

Path target = baseDir
    .resolve(fileName)
    .normalize();

if (!target.startsWith(baseDir)) {
    throw new SecurityException(
        "허용되지 않은 파일 경로입니다."
    );
}

byte[] data = Files.readAllBytes(target);

resolve()는 기준 경로와 입력값을 결합하고, normalize()는 현재 디렉터리와 상위 디렉터리 같은 경로 요소를 정리합니다. 이후 startsWith(baseDir)를 이용해 최종 경로가 기준 디렉터리 내부에 있는지 확인합니다.

여기서 중요한 점은 문자열의 시작 부분을 비교하는 것이 아니라 Path 객체를 기준으로 비교해야 한다는 것입니다.

다음과 같은 문자열 비교는 피해야 합니다.

if (target.toString().startsWith(
    "/var/app/downloads"
)) {
    // 파일 처리
}

비슷한 이름을 가진 다른 디렉터리가 조건을 통과할 가능성이 있기 때문입니다. 경로 비교에는 운영체제의 경로 구조를 이해하는 Path.startsWith()를 사용하는 편이 안전합니다.

심볼릭 링크까지 확인해야 하는 이유

normalize()만 적용하면 경로 문자열에 포함된 상위 디렉터리 요소는 정리할 수 있습니다. 하지만 기준 디렉터리 내부에 외부 위치를 가리키는 심볼릭 링크가 존재한다면 단순한 정규화만으로는 충분하지 않을 수 있습니다.

실제로 존재하는 파일을 제공하는 기능이라면 toRealPath()를 이용해 심볼릭 링크가 해석된 실제 경로를 확인할 수 있습니다.

Path baseDir = Paths.get(
    "/var/app/downloads"
).toRealPath();

Path target = baseDir
    .resolve(fileName)
    .normalize();

if (!target.startsWith(baseDir)) {
    throw new SecurityException(
        "허용되지 않은 파일 경로입니다."
    );
}

Path realTarget = target.toRealPath();

if (!realTarget.startsWith(baseDir)) {
    throw new SecurityException(
        "기준 디렉터리를 벗어난 파일입니다."
    );
}

byte[] data = Files.readAllBytes(realTarget);

첫 번째 검사는 입력값으로 만든 경로가 기준 위치를 벗어나는지 확인합니다. 두 번째 검사는 심볼릭 링크가 해석된 실제 파일 위치까지 확인합니다.

파일 생성 기능은 아직 대상 파일이 존재하지 않아 toRealPath()를 바로 사용할 수 없습니다. 이런 경우에는 상위 디렉터리의 실제 경로를 확인하고, 서버에서 파일명을 새로 생성하며, 심볼릭 링크 사용을 제한하는 별도의 설계가 필요합니다.

파일명 허용 목록 설정

사용자가 임의의 하위 경로를 입력할 필요가 없다면 파일명만 받도록 제한하는 것이 좋습니다.

if (fileName == null ||
    !fileName.matches(
        "^[a-zA-Z0-9._-]{1,100}$"
    )) {
    throw new IllegalArgumentException(
        "허용되지 않은 파일명입니다."
    );
}

이 예시는 영문자, 숫자, 점, 밑줄, 하이픈만 허용하고 길이를 100자로 제한합니다. 실제 서비스에서는 한글 파일명 지원 여부와 허용할 확장자를 고려해 규칙을 조정해야 합니다.

확장자 검사는 파일 형식을 제한하는 데 도움이 되지만 경로 검증을 대신할 수는 없습니다. 파일명이 허용된 확장자로 끝나더라도 최종 경로가 기준 디렉터리를 벗어나면 차단해야 합니다.

안전한 구성은 다음 방어를 함께 적용하는 것입니다.

  1. 파일 식별자로 조회합니다.
  2. 사용자와 파일의 접근권한을 확인합니다.
  3. 입력값을 허용 목록으로 검사합니다.
  4. 최종 경로를 정규화합니다.
  5. 기준 디렉터리 내부인지 확인합니다.
  6. 심볼릭 링크가 해석된 실제 경로도 검사합니다.

파일 접근권한 검증도 필요하다

경로 조작을 차단했다고 해서 다른 사용자의 파일 접근까지 자동으로 막아지는 것은 아닙니다.

예를 들어 사용자가 파일 번호를 변경해 다른 회원의 첨부파일을 내려받을 수 있다면 경로 조작이 아니라 접근통제 취약점에 해당합니다. 서버는 요청한 파일이 현재 사용자에게 허용된 파일인지 반드시 확인해야 합니다.

안전한 파일 다운로드 기능에는 두 가지 검증이 모두 필요합니다.

  • 요청한 파일이 허용된 저장 디렉터리 안에 있는가?
  • 현재 사용자가 해당 파일을 열람할 권한이 있는가?

첫 번째는 경로 검증이고 두 번째는 접근권한 검증입니다. 어느 한쪽만 적용하면 다른 방식으로 파일이 노출될 수 있습니다.

오류 메시지와 파일 권한 관리

존재하지 않는 파일을 요청했을 때 서버의 실제 경로를 오류 화면에 표시해서는 안 됩니다.

다음과 같은 정보는 공격자가 서버 구조를 파악하는 데 도움이 될 수 있습니다.

  • 서버의 절대경로
  • 운영체제 종류
  • 사용자 계정명
  • 파일 저장 디렉터리
  • 프레임워크 내부 경로
  • 예외 발생 코드 위치

사용자에게는 “파일을 찾을 수 없습니다” 정도의 일반적인 안내만 제공하고, 상세 오류는 접근이 제한된 서버 로그에 기록하는 것이 좋습니다.

웹 애플리케이션 계정에는 서비스에 필요한 디렉터리의 읽기 및 쓰기 권한만 부여해야 합니다. 애플리케이션이 서버 전체 파일을 읽을 수 있는 권한으로 실행된다면 경로 조작이 발생했을 때 피해 범위가 커집니다.

압축 해제 과정의 Zip Slip

경로 조작은 일반적인 파일 다운로드뿐만 아니라 압축파일을 해제할 때도 발생할 수 있습니다. 압축파일 내부 항목의 이름에 상위 디렉터리 경로가 포함돼 있는데 이를 검증하지 않고 해제하면 지정된 폴더 밖에 파일이 생성될 수 있습니다.

이러한 취약점을 흔히 Zip Slip이라고 부릅니다.

압축파일을 해제할 때도 각 항목의 최종 경로를 계산하고 압축 해제 디렉터리 내부인지 확인해야 합니다. 압축파일 자체의 확장자만 확인하는 것으로는 부족합니다.

경로 조작 취약점 점검 항목

소스 코드를 점검할 때는 먼저 외부 입력값을 사용하는 파일 처리 기능을 찾습니다.

자바에서는 File, FileInputStream, FileOutputStream, Paths.get(), Files.readAllBytes(), Files.copy() 등이 주요 점검 대상입니다.

이후 HTTP 요청값이 어떤 경로로 파일 처리 함수까지 전달되는지 확인합니다.

URL 파라미터 또는 파일명
→ 변수 저장
→ 기준 경로와 결합
→ 파일 읽기·쓰기·삭제

점검 시에는 다음 사항을 확인해야 합니다.

  • 사용자가 파일명이나 경로를 직접 지정할 수 있는가?
  • 입력값에 경로 구분자를 허용하고 있는가?
  • 디코딩 전후의 값을 일관되게 검사하는가?
  • 최종 경로를 normalize()로 정규화하는가?
  • 정규화한 경로가 기준 디렉터리 안에 있는가?
  • 심볼릭 링크가 해석된 실제 경로를 확인하는가?
  • 파일 식별자와 실제 저장 이름을 분리했는가?
  • 현재 사용자의 파일 접근권한을 검사하는가?
  • 다운로드와 업로드 기능에 동일한 경로 정책을 적용했는가?
  • 오류 메시지에 서버의 절대경로가 노출되는가?
  • 애플리케이션 계정에 과도한 파일 권한이 있는가?

취약점 점검은 자신이 소유하거나 명시적인 허가를 받은 시스템에서만 진행해야 합니다.

자주 묻는 질문

../ 문자열을 차단하면 디렉터리 트래버설을 막을 수 있나요?

충분하지 않습니다. 운영체제별 경로 구분자, 중첩된 문자열, 인코딩과 디코딩 처리 순서에 따라 우회될 수 있습니다. 최종 경로를 정규화한 뒤 기준 디렉터리 내부인지 확인해야 합니다.

파일 확장자를 검사하면 안전한가요?

확장자 검사는 허용할 파일 형식을 제한하는 용도입니다. 경로가 저장 디렉터리 밖으로 이동하는 문제까지 해결하지는 못합니다. 확장자 검사와 경로 검증을 함께 적용해야 합니다.

Paths.get()을 사용하면 자동으로 안전해지나요?

아닙니다. Paths.get()은 문자열을 경로 객체로 변환할 뿐 외부 입력값의 안전성을 판단하지 않습니다. resolve(), normalize(), startsWith() 등을 이용한 추가 검증이 필요합니다.

경로 조작과 파일 삽입은 같은 취약점인가요?

서로 관련될 수 있지만 의미는 다릅니다. 경로 조작은 허용된 디렉터리 밖의 파일 경로에 접근하는 문제이고, 파일 삽입은 외부 입력값으로 선택된 파일을 프로그램이 포함하거나 실행하는 과정에서 발생합니다.

파일 번호만 사용하면 안전한가요?

경로 조작 위험은 줄어들지만 접근권한 검증이 없다면 다른 사용자의 파일을 조회하는 문제가 발생할 수 있습니다. 파일 번호의 형식뿐 아니라 현재 사용자가 해당 파일에 접근할 권한이 있는지도 확인해야 합니다.

마무리

경로 조작 취약점은 사용자가 전달한 파일명이나 경로가 서버의 실제 파일 위치를 결정할 때 발생합니다. 상대경로를 이용해 허용된 디렉터리 밖으로 이동하는 방식은 디렉터리 트래버설이라고 부릅니다.

가장 안전한 방법은 사용자에게 실제 파일 경로를 받지 않는 것입니다. 파일 식별자를 전달받고 서버가 데이터베이스에서 저장 위치를 조회하도록 설계하면 경로 조작 가능성을 크게 줄일 수 있습니다.

파일명이나 하위 경로를 받아야 한다면 입력값 허용 목록, 경로 정규화, 기준 디렉터리 확인, 실제 경로 검증을 함께 적용해야 합니다. 파일 접근권한과 운영체제 권한도 별도로 제한해야 합니다.

코드를 점검할 때는 ../ 문자열만 찾는 데서 끝내지 말고 외부 입력값이 파일 읽기, 쓰기, 복사, 삭제 기능까지 어떤 과정을 거쳐 전달되는지 확인해야 합니다. 최종 파일 경로가 어디를 가리키는지 직접 계산해 보는 것이 경로 조작 취약점을 발견하는 가장 확실한 방법입니다.

핵심 요약: 사용자 입력값으로 서버의 실제 파일 경로를 직접 만들지 말고, 파일 식별자 방식과 최종 경로 정규화, 기준 디렉터리 검증을 함께 적용해야 합니다.

핵심 키워드: 경로 조작 취약점, 디렉터리 트래버설, Path Traversal, Directory Traversal, 파일 다운로드 취약점, 경로 검증

태그: 웹 보안, 웹 취약점, 경로 조작, 디렉터리 트래버설, 파일 보안, 시큐어 코딩

댓글 남기기