ガイドライン

インジェクション - パストラバーサル

パストラバーサルは、インジェクション脆弱性のもう一つのかなり一般的なタイプです。URIの構築(URLであれ、ファイルパスであれ、その他であれ)が、完全に解決されたパスが意図したパスのルート外を指していないことを適切に保証していない場合に起こりがちです。 

パス・トラバーサルは、実際にはパス*インジェクション*の脆弱性であるとも考えられることを指摘しておくことが重要だ。 

パストラバーサル脆弱性の影響は、トラバーサルが発生する場所のコンテキストと、行われた全体的なハードニングに大きく依存する。しかし、その話をする前に、この脆弱性の実際的な例を簡単に見てみよう:                                                                                                        

簡単な内訳

契約書や求人票のテンプレートのようなドキュメントを提供するアプリケーションのエンドポイントを考えてみましょう。これらはすべて、アプリケーション内で静的なPDFのようなファイルかもしれません。 

このような状況では、リクエストに応じてファイルを取得するために、次のようなコードを書くことになる:

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

return file.read(path);

この脆弱性がどのように作用するかを示すために、アプリケーションのルートがどこにあるか知っておく必要がある。 

このアプリケーションが「ファイル名」パラメーターを受け取ることは分かっている:

ファイル名 未解決のパス 解決済みのパス
プライバシー.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'フォルダから、Linuxではパスワードのハッシュが保存されている'shadow'ファイルを含む'/etc/'フォルダに移動することができる。ご想像の通り、これは本当に理想的ではない。 

ウルのトラバーサルを見る

パストラバーサルの別の変形は、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

2番目の例の入力では、ID番号'1'の注文をフェッチするのではなく、代わりにdeleteメソッドを呼び出している。

緩和策

パス・トラバーサルについて論じる場合、直接的な緩和策と間接的な防御策の両方がある。まず、パスの扱い方を見てみよう。

直接緩和

パスを扱うとなると、パス解決(パスの正規化)のプロセスとその重要性を理解しなければならない。 

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 "Tried to read outside of base folder";
else
return file.read(resolvedPath);

アンチパターン - ファイル名をサニタイズしようとする

こんなことをしたくなるかもしれない:


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

しかし、この方法は使うべきではない。パスを扱う上で重要なのは、常に正規パスを見ることだ。 

正規パスがルール違反でない限り、最終的にパスがどのように構築されるかに違いはない。このようにパスをサニタイズしようとすることは、非常にエラーを起こしやすく、安全であることはほとんどない。

アクセス制限

これまでの例では、「/etc/shadow」ファイルの読み取りを使ってきた。このファイルは、Linuxのパスワード・ハッシュが格納されているファイルである。しかし、アプリケーションがこのファイルやルート外の他のファイルを読むことができなければならない理由はない。

コンテナを採用している場合、すでに多くのリスクを軽減している可能性が高い。コンテナを堅牢化する(root権限で実行しない、など)手順を踏むことは極めて重要だ。ウェブ・プロセスからすべての権限を削除し、ファイル・システムの読み取りパーミッションを厳密に必要なファイルだけに制限することを強く推奨する。 

それでは、様々な言語でいくつかの例を紹介し、実際に使用しながら、より良いデモンストレーションを行えるようにしよう。

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 "Trying to read file outside of base folder";

var fileContents = File.ReadAllText(canonicalPath);

C# - セキュア - ファイル名

この例では、パスのファイル名部分のみを取り出し、指定されたフォルダーの外へのトラバースが不可能になるようにすることで、パストラバーサルから保護している。 

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

// 他のサブフォルダへのナビゲートを許可しない場合のみ使用
var fileName = Path.GetFileName("../../../../etc/passwd");

// SECURE:Reads /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 - セキュア - Canonical

この例では、完全な(絶対)パスを解決し、解決されたファイルのパスがベースフォルダー内にあることを確認することで、パストラバーサルから保護しています。 

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:etc/passwdを読み込む
const data = fs.readFileSync(baseFolder + fileName, 'utf8');

ジャバスクリプト - セキュア - 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));

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

Javascript - セキュア - ファイル名

この例では、パスのファイル名部分のみを取り出し、指定されたフォルダーの外へのトラバースが不可能になるようにすることで、パストラバーサルから保護している。 

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');

パイソン - 安全ではない

フルパスを解決しない、あるいはパスのファイル名部分のみを使用するようにすることで、コードはパストラバーサルに対して脆弱なままとなる。 

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

# INSECURE: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)

# SECURE:
if not normalizedPath.startswith(baseFolder):
return "Trying to read out of base folder"

# 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()