インジェクション - パストラバーサル
パストラバーサルは、非常によく見られるインジェクション脆弱性の一種です。これは、URI(URL、ファイルパスなど)を構築する際に、解決されたパスが意図したルートディレクトリの外側を指していないことを適切に検証できていない場合に発生します。
パストラバーサルは、実質的にパス「インジェクション」脆弱性とも見なせる点に留意することが重要です。
パストラバーサル脆弱性の影響は、その脆弱性がどこで発生するか、またどのような堅牢化対策が施されているかに大きく依存します。その詳細に入る前に、この脆弱性がどのようなものか、具体的な例を見てみましょう。
概要
アプリケーション内で、契約書や求人票のテンプレートといったドキュメントを提供するエンドポイントを想定してください。これらは、アプリケーション内に静的ファイルとして保存されているPDFファイルである可能性があります。
この状況では、リクエストに応じてファイルを取得するために、以下のようなコードが使われるかもしれません。
let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename;
return file.read(path);
この脆弱性がどのように機能するかを示すために、アプリケーションのルートディレクトリを「/var/www/api/」と仮定します。
アプリケーションが「filename」パラメータを受け取る場合、いくつかの入力例と、その結果を見てみましょう。
「../」を使用してファイルシステムをトラバース(移動)できる点に注目してください。これにより、通常PDFが保存されている「documents」フォルダから抜け出し、Linuxでパスワードハッシュが格納されている「/etc/」フォルダ内の「shadow」ファイルにアクセスできてしまいます。お察しの通り、これは非常に危険な状態です。
URLにおけるトラバーサル
パストラバーサルの別のバリエーションとして、APIとやり取りするためのURLを構築する際に発生するものがあります。以下のメソッドを持つAPIを想定してください。
この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を示します。
正規化(カノニカライゼーション)は通常クライアント側では行われませんが(行われる場合もあります)、Webサーバーはリクエストを以下の形式に正規化します。
2番目の入力例では、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 "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("../", "");
...
しかし、このアプローチは決して使用すべきではありません。パスを扱う際の鍵は、常に正規化されたパスを確認することです。
正規化されたパスがルールに違反していない限り、最終的にパスがどのように構築されるかは重要ではありません。このようにパスをサニタイズしようとする手法はエラーが発生しやすく、安全性を確保できることはほとんどありません。
アクセス制限
これまでの例では、Linuxのパスワードハッシュが格納された「/etc/shadow」ファイルの読み取りを例に挙げました。しかし、アプリケーションがルートディレクトリ外のファイルや、本来不要なファイルを読み取る必要は本来ありません。
コンテナを使用している場合、多くのリスクはすでに軽減されているはずです。コンテナの堅牢化(root権限で実行しないなど)は不可欠です。Webプロセスからすべての特権を剥奪し、ファイルシステムへの読み取り権限を必要最小限のファイルに限定することを強く推奨します。
例
理解を深めるために、いくつかのプログラミング言語での実装例を紹介します。
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# - 安全な実装 - ファイル名
この例では、パスからファイル名部分のみを抽出することでパストラバーサル攻撃を防いでいます。これにより、指定されたフォルダ外へ移動することは不可能になります。
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 - 安全な実装 - 正規化
この例では、フルパス(絶対パス)を解決し、解決されたパスがベースフォルダ内にあることを確認することで、パストラバーサルを防いでいます。
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 - 安全な実装 - ファイル名
この例では、パスからファイル名部分のみを抽出することでパストラバーサル攻撃を防いでいます。これにより、指定されたフォルダ外へ移動することは不可能になります。
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 - 安全な実装 - 正規化
この例では、フルパス(絶対パス)を解決し、解決されたパスがベースフォルダ内にあることを確認することで、パストラバーサルを防いでいます。
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 - 安全な実装 - ファイル名
この例では、パスからファイル名部分のみを抽出することでパストラバーサル攻撃を防いでいます。これにより、指定されたフォルダ外へ移動することは不可能になります。
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 - 安全 - 正規化
この例では、フルパス(絶対パス)を解決し、解決されたパスがベースフォルダ内にあることを確認することで、パストラバーサルを防いでいます。
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 - 安全 - ファイル名
この例では、パスからファイル名部分のみを抽出することでパストラバーサル攻撃を防いでいます。これにより、指定されたフォルダ外へ移動することは不可能になります。
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のようなソフトウェアの脆弱性を理解し軽減するためのセキュアコーディングガイドラインを確認しましょう。また、実践的なアプリシミュレーションを通じて学べるトレーニングミッションにもぜひ挑戦してください。