가이드라인

주입 - 경로 탐색

경로 통과는 또 다른 매우 일반적인 유형의 인젝션 취약점입니다. URL, 파일 경로 등 URI를 구성할 때 완전히 확인된 경로가 의도한 경로의 루트 외부를 가리키지 않도록 제대로 확인하지 않을 때 발생하는 경향이 있습니다. 

경로 통과는 사실상 경로 *인젝션* 취약점으로 볼 수도 있다는 점을 상기하는 것이 중요합니다. 

경로 트래버스 취약점의 영향은 트래버스가 발생하는 컨텍스트와 전반적인 강화 정도에 따라 크게 달라집니다. 하지만 이에 대해 알아보기 전에 이 취약점에 대한 간단한 실제 사례를 통해 우리가 무슨 이야기를 하고 있는지 살펴보겠습니다:                                                                                                        

간단한 분석

애플리케이션에서 계약서나 채용 공고 템플릿과 같은 문서를 제공하는 엔드포인트를 생각해 보세요. 이러한 문서는 모두 애플리케이션에 정적으로 저장된 PDF와 같은 파일일 수 있습니다. 

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

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

return file.read(path);

취약점이 어떻게 발생하는지 설명하기 위해서는 애플리케이션의 루트가 어디에 있는지 알아야 하므로 이 예제에서는 애플리케이션의 루트가 '/var/www/api/'에 있다고 가정합니다. 

애플리케이션이 '파일 이름' 매개 변수를 사용한다는 것을 알고 있으므로 몇 가지 입력 예와 그 결과를 살펴 보겠습니다:

파일 이름 해결되지 않은 경로 확인된 경로
개인정보.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
../../../../등/shadow /var/www/api/documents/../../../../etc/shadow /etc/shadow

'../'를 사용하여 파일 시스템을 어떻게 횡단할 수 있는지 주목하세요. 일반적으로 PDF가 있는 '문서' 폴더를 벗어나 Linux에서는 암호 해시가 포함된 '섀도' 파일이 있는 '/etc/' 폴더로 이동할 수 있습니다. 상상할 수 있듯이 이는 정말 이상적이지 않습니다. 

URL에서 트래버스 살펴보기

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

URL 패턴 설명
/api/v1/order/get/{id} 지정된 ID를 가진 주문에 대한 세부 정보를 가져옵니다.
/api/v1/order/delete/{id} 특정 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을 확인할 수 있습니다. 

표준화는 일반적으로 클라이언트 측에서 수행되지 않지만(수행될 수 있지만), 웹 서버는 요청을 아래와 같은 형식으로 표준화합니다.

주문 ID 번호 실제 호출된 URL
1 /api/v1/order/get/1
1/../../삭제/1 /api/v1/order/delete/1

두 번째 예제의 입력에서는 ID 번호가 '1'인 주문을 가져오는 대신 실제로 삭제 메서드를 호출하여 주문을 삭제합니다.

완화

경로 통과에 대해 논의할 때는 직접적인 완화 방법과 함께 가능한 한 자주 적용할 수 있는 간접/방어 기술이 있습니다. 먼저 경로를 처리하는 방법을 살펴보겠습니다.

직접 완화

경로를 처리할 때는 경로 확인 또는 경로 표준화 프로세스와 그 중요성을 이해해야 합니다. 

'/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("../", "");
...

그러나 이 접근 방식은 사용해서는 됩니다. 경로 처리의 핵심은 항상 표준 경로를 살펴보는 것입니다. 

표준 경로가 규칙을 위반하지 않는 한, 경로가 궁극적으로 어떻게 구성되는지는 실제로 아무런 차이가 없습니다. 이와 같은 경로를 위생 처리하려는 시도는 오류가 발생하기 쉽고 안전하지 않은 경우가 거의 없습니다.

액세스 제한

이전 예제에서는 Linux에서 비밀번호 해시가 있는 파일인 '/etc/shadow' 파일을 읽는 방법을 사용했습니다. 하지만 애플리케이션이 해당 파일이나 다른 파일을 루트 외부에서 읽을 수 있어야 할 이유는 없습니다.

컨테이너를 사용하는 경우 이미 많은 위험을 완화하고 있을 가능성이 높습니다. 루트 권한으로 실행하지 않는 등 컨테이너를 강화하는 조치를 취하는 것이 중요합니다. 웹 프로세스에서 모든 권한을 삭제하고 파일 시스템에 대한 읽기 권한을 꼭 필요한 파일로만 제한하는 것이 좋습니다. 

예제

이제 다양한 언어로 된 몇 가지 예시를 공유하여 실제로 작동하는 동안 좀 더 잘 이해할 수 있도록 도와드리겠습니다.

C# - 안전하지 않음

전체 경로를 확인하지 않거나 경로의 파일 이름 일부만 사용하도록 하면 코드가 경로 탐색에 취약해집니다. 

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

// INSECURE: Reads /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));

// SECURE:
if(!canonicalPath.StartsWith(baseFolder))
return "기본 폴더 외부에서 파일 읽기 시도 중";

var fileContents = File.ReadAllText(canonicalPath);

C# - 보안 - 파일 이름

이 예에서는 경로에서 파일 이름 부분만 가져와서 지정된 폴더 밖으로 이동할 수 없도록 하여 경로 순회를 방지합니다. 

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

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

// SECURE: var/www/app/documents/passwd를 읽습니다
var fileContents = File.ReadAllText(Path.Combine(baseFolder, fileName));

Java - 안전하지 않음

전체 경로를 확인하지 않거나 경로의 파일 이름 일부만 사용하도록 하면 코드가 경로 탐색에 취약해집니다. 

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

// INSECURE: Reads /etc/passwd
Path filePath = Paths.get(baseFolder + fileName);
List<String> lines = Files.readAllLines(filePath);

Java - 보안 - 표준

이 예에서는 전체(절대) 경로를 확인하고 파일 확인 경로가 기본 폴더 내에 있는지 확인하여 경로 순회를 방지합니다. 

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

// INSECURE: Reads /etc/passwd
Path normalizedPath  = Paths.get(baseFolder + fileName).normalize();
if(!normalizedPath.toString().startsWith(baseFolder))
{
    return "Trying to read path outside of root";
}
else
{
    List<String> lines = Files.readAllLines(normalizedPath);
}

Java - 보안 - 파일 이름

이 예에서는 경로에서 파일 이름 부분만 가져와서 지정된 폴더 밖으로 이동할 수 없도록 하여 경로 순회를 방지합니다. 

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

// Only use this if you don't allow navigating into other subfolders
String fileName = Paths.get("../../../../../etc/passwd").getFileName().toString();

// SECURE: Reads /var/www/app/documents/passwd
Path filePath = Paths.get(baseFolder + fileName);
List<String> lines = Files.readAllLines(filePath);

자바스크립트 - 안전하지 않음

전체 경로를 확인하지 않거나 경로의 파일 이름 일부만 사용하도록 하면 코드가 경로 탐색에 취약해집니다. 

const fs = require('fs');

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

// INSECURE: Reads /etc/passwd
const data = fs.readFileSync(baseFolder + fileName, 'utf8');

자바스크립트 - 보안 - 표준

이 예에서는 전체(절대) 경로를 확인하고 파일 확인 경로가 기본 폴더 내에 있는지 확인하여 경로 순회를 방지합니다. 

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

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

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

// SECURE: Reads /var/www/app/documents/passwd
const data = fs.readFileSync(normalizedPath, 'utf8');

자바스크립트 - 보안 - 파일 이름

이 예에서는 경로에서 파일 이름 부분만 가져와서 지정된 폴더 밖으로 이동할 수 없도록 하여 경로 순회를 방지합니다. 

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

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

// SECURE: Reads /var/www/app/documents/passwd
const data = fs.readFileSync(path.join(baseFolder, fileName), 'utf8');

Python - 안전하지 않음

전체 경로를 확인하지 않거나 경로의 파일 이름 일부만 사용하도록 하면 코드가 경로 탐색에 취약해집니다. 

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

# INSECURE: /etc/passwd를 읽습니다
fileContents = open(baseFolder + fileName).read()

Python - 보안 - 표준

이 예에서는 전체(절대) 경로를 확인하고 파일 확인 경로가 기본 폴더 내에 있는지 확인하여 경로 순회를 방지합니다. 

import os.path

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

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

# SECURE: 지정된 기본 폴더 외부에서 파일을 읽으려는 모든 시도를 거부합니다
if not normalizedPath.startswith(baseFolder):
반환 "기본 폴더에서 읽으려는 중"

# SECURE: /var/www/app/documents/passwd를 읽습니다
fileContents = open(normalizedPath).read()

Python - 보안 - 파일 이름

이 예에서는 경로에서 파일 이름 부분만 가져와서 지정된 폴더 밖으로 이동할 수 없도록 하여 경로 순회를 방지합니다. 

import os.path

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

# SECURE: 읽습니다 /var/www/app/documents/passwd
fileContents = open(os.path.join(baseFolder, fileName)).read()