가이드라인

인젝션 - 경로 탐색(Path Traversal)

경로 탐색(Path Traversal)은 꽤 흔한 유형의 주입 취약점입니다. 이는 URI(URL, 파일 경로 등)를 구성할 때, 완전히 해석된 경로가 의도된 경로의 루트 외부를 가리키지 않도록 적절히 보장하지 못할 때 발생합니다.

경로 탐색은 사실상 경로 *주입(path injection)* 취약점으로도 볼 수 있다는 점을 언급할 필요가 있습니다.

경로 탐색 취약점의 영향은 탐색이 발생하는 맥락과 적용된 전반적인 보안 강화 수준에 크게 좌우됩니다. 본격적으로 다루기에 앞서, 이 취약점이 무엇을 의미하는지 간단한 실무 예시를 통해 살펴보겠습니다.

간단한 분석

계약서나 채용 제안서 템플릿과 같은 문서를 제공하는 애플리케이션의 엔드포인트를 생각해 보세요. 이들은 모두 애플리케이션 내에 정적으로 존재하는 PDF 파일일 수 있습니다.

이런 상황에서 요청 시 파일을 가져오기 위해 다음과 같은 코드를 사용할 수 있습니다:

let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename;

return file.read(path);

취약점이 어떻게 나타나는지 보여주기 위해 애플리케이션의 루트 위치를 알아야 합니다. 이 예시에서는 애플리케이션의 루트가 ‘/var/www/api/’라고 가정하겠습니다.

애플리케이션이 ‘filename’ 매개변수를 받는다는 것을 알고 있으니, 몇 가지 입력 예시와 그 결과를 살펴보겠습니다:

Filename Unresolved path Resolved path
Privacy.pdf /var/www/api/documents/Privacy.pdf /var/www/api/documents/Privacy.pdf
../config/prod.config /var/www/api/documents/../config/prod.config /var/www/api/config/prod.config
../../../../etc/shadow /var/www/api/documents/../../../../etc/shadow /etc/shadow

‘../’를 사용하여 파일 시스템을 탐색하는 방법에 주목하세요. PDF가 저장된 ‘documents’ 폴더를 벗어나, 리눅스에서 비밀번호 해시를 포함하는 ‘shadow’ 파일이 있는 ‘/etc/’ 폴더로 이동할 수 있습니다. 상상하시겠지만, 이는 매우 위험한 상황입니다.

URL에서의 탐색 살펴보기

경로 탐색의 또 다른 변형은 API와 상호작용하기 위한 URL을 구성할 때 발생할 수 있습니다. 다음 메서드를 가진 API가 있다고 가정해 보겠습니다:

URL pattern Description
/api/v1/order/get/{id} Gets details about the order with the specified ID
/api/v1/order/delete/{id} Deletes an order with the specific ID

이 API는 주문 정보를 가져오려는 다른 애플리케이션에 의해 호출될 수 있습니다:

let apiBase = "https://my.api/api/v1";
let orderApi = apiBase + "/order/get";

let apiUrl = orderApi + request.params.orderId;

let response = http.get(apiUrl);

사용자가 제공한 주문 ID에 따라 어떤 일이 벌어질까요? 아래에서 입력값에 따라 실제로 호출되는 URL을 확인해 보세요.

정규화(canonicalization)는 보통 클라이언트 측에서 수행되지 않지만(수행될 수도 있음), 웹 서버는 요청을 아래와 같은 형식으로 정규화합니다.

Order ID number Actual URL invoked
1 /api/v1/order/get/1
1/../../delete/1 /api/v1/order/delete/1

두 번째 예시의 입력을 사용하면, ID가 ‘1’인 주문을 가져오는 대신 실제로 삭제 메서드가 호출되어 결과적으로 주문이 삭제됩니다.

완화 조치

경로 탐색(Path Traversal) 문제를 다룰 때는 직접적인 완화 조치와 함께 가능한 한 자주 적용해야 할 간접적/방어적 기법이 모두 존재합니다. 먼저 경로를 처리하는 방법부터 살펴보겠습니다.

직접적인 완화 조치

경로를 처리할 때는 경로 해석(path resolution) 또는 경로 정규화(path canonicalization) 과정과 그 중요성을 이해해야 합니다.

'/var/www/api/documents/../../../../etc/shadow'와 같은 경로는 비정규화된 경로입니다. 파일 시스템에 이 경로를 요청하면 시스템은 이를 '/etc/shadow'로 정규화합니다. 비정규화된 경로를 직접 열려고 시도하지 않는 것이 중요합니다. 대신 먼저 경로를 정규화하고, 의도한 파일이나 폴더만을 가리키는지 확인한 후 읽어야 합니다.

‍

let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename;

let resolvedPath = path.resolve(path);

if(!resolvedPath.startswith(baseFolder))
return "기본 폴더 외부의 파일을 읽으려 했습니다";
else
return file.read(resolvedPath);

안티 패턴 - 파일 이름 정규화 시도

다음과 같이 처리하고 싶은 유혹이 들 수 있습니다:


let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename.replace("../", "");
...

하지만 이러한 접근 방식은 사용해서는 안 됩니다. 경로를 처리할 때 핵심은 항상 정규화된 경로를 확인하는 것입니다.

정규화된 경로가 규칙을 위반하지 않는 한, 경로가 최종적으로 어떻게 구성되는지는 중요하지 않습니다. 이처럼 경로를 정규화하려고 시도하는 것은 오류가 발생하기 쉽고, 보안상 안전하지 않은 경우가 대부분입니다.

액세스 제한

앞선 예제에서는 리눅스의 비밀번호 해시가 포함된 '/etc/shadow' 파일을 읽는 경우를 사용했습니다. 하지만 애플리케이션이 루트 디렉터리 외부의 해당 파일이나 다른 파일을 읽어야 할 이유는 전혀 없습니다.

컨테이너를 사용 중이라면 이미 많은 위험을 완화하고 있을 가능성이 큽니다. 컨테이너 강화 조치(루트 권한으로 실행하지 않는 등)를 취하는 것이 중요합니다. 웹 프로세스에서 모든 권한을 제거하고, 파일 시스템에 대한 읽기 권한을 반드시 필요한 파일로만 제한하는 것을 강력히 권장합니다.

예시

이제 다양한 언어의 예제를 통해 실제 작동 방식을 더 명확하게 보여드리겠습니다.

C# - 보안 취약

전체 경로를 해석하지 않거나 경로의 파일 이름 부분만 사용하도록 보장하지 않으면, 코드는 경로 탐색 공격에 취약해집니다.

var baseFolder = "/var/www/app/documents/";
var fileName = "../../../../../etc/passwd";

// 보안 취약: /etc/passwd를 읽음
var fileContents = File.ReadAllText(Path.Combine(baseFolder, fileName));

C# - 보안 적용 - 정규화

이 예제에서는 전체(절대) 경로를 해석하고, 해석된 파일 경로가 기본 폴더 내에 있는지 확인하여 경로 탐색을 방지합니다.

var baseFolder = "/var/www/app/documents/";
var fileName = "../../../../../etc/passwd";

var canonicalPath = Path.GetFullPath(Path.Combine(baseFolder, fileName));

// 보안 적용: 지정된 기본 폴더 외부를 읽으려는 모든 시도를 차단
if(!canonicalPath.StartsWith(baseFolder))
return "기본 폴더 외부의 파일을 읽으려 합니다";

var fileContents = File.ReadAllText(canonicalPath);

C# - 보안 적용 - 파일 이름

이 예제에서는 경로에서 파일 이름 부분만 추출하여 경로 탐색(Path Traversal) 공격을 방어합니다. 이를 통해 지정된 폴더 외부로 이동하는 것이 불가능해집니다. 

var baseFolder = "/var/www/app/documents/";

// 다른 하위 폴더로 이동을 허용하지 않는 경우에만 사용하세요
var fileName = Path.GetFileName("../../../../../etc/passwd");

// 안전함: /var/www/app/documents/passwd를 읽음
var fileContents = File.ReadAllText(Path.Combine(baseFolder, fileName));

Java - 취약함

전체 경로를 해석하지 않거나 경로의 파일 이름 부분만 사용하도록 보장하지 않으면, 코드는 경로 탐색 공격에 취약해집니다.

String baseFolder = "/var/www/app/documents/";
String fileName = "../../../../../etc/passwd";

// 취약함: /etc/passwd를 읽음
Path filePath = Paths.get(baseFolder + fileName);
List<String> lines = Files.readAllLines(filePath);

Java - 안전함 - 정규화(Canonical)

이 예제에서는 전체(절대) 경로를 해석하고, 해석된 파일 경로가 기본 폴더 내에 있는지 확인하여 경로 탐색을 방지합니다.

String baseFolder = "/var/www/app/documents/";
String fileName = "../../../../../etc/passwd";

// 취약함: /etc/passwd를 읽음
Path normalizedPath  = Paths.get(baseFolder + fileName).normalize();
if(!normalizedPath.toString().startsWith(baseFolder))
{
    return "루트 외부의 경로를 읽으려 시도함";
}
else
{
    List<String> lines = Files.readAllLines(normalizedPath);
}

Java - 안전함 - 파일 이름

이 예제에서는 경로에서 파일 이름 부분만 추출하여 경로 탐색(Path Traversal) 공격을 방어합니다. 이를 통해 지정된 폴더 외부로 이동하는 것이 불가능해집니다. 

String baseFolder = "/var/www/app/documents/";

// 다른 하위 폴더로 이동을 허용하지 않는 경우에만 사용하세요
String fileName = Paths.get("../../../../../etc/passwd").getFileName().toString();

// 안전함: /var/www/app/documents/passwd를 읽음
Path filePath = Paths.get(baseFolder + fileName);
List<String> lines = Files.readAllLines(filePath);

Javascript - 취약함

전체 경로를 해석하지 않거나 경로의 파일 이름 부분만 사용하도록 보장하지 않으면, 코드는 경로 탐색 공격에 취약해집니다.

const fs = require('fs');

const baseFolder = "/var/www/app/documents/";
const fileName = "../../../../../etc/passwd";

// 취약함: /etc/passwd를 읽음
const data = fs.readFileSync(baseFolder + fileName, 'utf8');

Javascript - 안전함 - 정규화(Canonical)

이 예제에서는 전체(절대) 경로를 해석하고, 해석된 파일 경로가 기본 폴더 내에 있는지 확인하여 경로 탐색을 방지합니다.

const fs = require("fs");
const path  = require("path");

const baseFolder = "/var/www/app/documents/";
const fileName = "../../../../../etc/passwd";

const normalizedPath = path.normalize(path.join(baseFolder, fileName));

// 안전함: /var/www/app/documents/passwd를 읽음
const data = fs.readFileSync(normalizedPath, 'utf8');

Javascript - 안전함 - 파일 이름

이 예제에서는 경로에서 파일 이름 부분만 추출하여 경로 탐색(Path Traversal) 공격을 방어합니다. 이를 통해 지정된 폴더 외부로 이동하는 것이 불가능해집니다. 

const fs = require("fs");
const path  = require("path");

const baseFolder = "/var/www/app/documents/";
const fileName = path.basename("../../../../../etc/passwd");

// 안전함: /var/www/app/documents/passwd를 읽음
const data = fs.readFileSync(path.join(baseFolder, fileName), 'utf8');

Python - 보안 취약

전체 경로를 해석하지 않거나 경로의 파일 이름 부분만 사용하도록 보장하지 않으면, 코드는 경로 탐색 공격에 취약해집니다.

baseFolder = "/var/www/app/documents/"
fileName = "../../../../../etc/passwd"

# 보안 취약: /etc/passwd 파일을 읽음
fileContents = open(baseFolder + fileName).read()

Python - 보안 - 정규화(Canonical)

이 예제에서는 전체(절대) 경로를 해석하고, 해석된 파일 경로가 기본 폴더 내에 있는지 확인하여 경로 탐색을 방지합니다.

import os.path

baseFolder = "/var/www/app/documents/"
fileName = "../../../../../etc/passwd"

normalizedPath =  os.path.normpath(baseFolder + fileName)

# 보안: 기본 폴더 외부의 파일에 접근하려는 시도를 차단
if not normalizedPath.startswith(baseFolder):
    return "기본 폴더 외부 접근 시도"

# 보안: /var/www/app/documents/passwd 파일을 읽음
fileContents = open(normalizedPath).read()

Python - 보안 - 파일 이름

이 예제에서는 경로에서 파일 이름 부분만 추출하여 경로 탐색(Path Traversal) 공격을 방어합니다. 이를 통해 지정된 폴더 외부로 이동하는 것이 불가능해집니다. 

import os.path

baseFolder = "/var/www/app/documents/"
fileName = os.path.basename("../../../../../etc/passwd")

# 보안: /var/www/app/documents/passwd 파일을 읽음
fileContents = open(os.path.join(baseFolder,  fileName)).read()
태그라인

실무에 바로 적용하는 보안 코딩

OWASP Top 10과 같은 소프트웨어 취약점을 이해하고 완화하기 위한 보안 코딩 가이드라인을 살펴보고, 실제 앱 시뮬레이션을 통해 실습할 수 있는 가이드형 교육 미션을 시작해 보세요.

추가 가이드라인
데모 예약하기