Path Traversal (パス トラバーサル)¶
概要¶
ユーザーによって制御されるデータから構築されたパスは、攻撃者にアプリケーション コード、データ、認証情報を含む構成ファイル、機密性の高い OS ファイルなどの予期しないリソースへのアクセスを許す可能性があります。パス トラバーサルは、機密情報の公開、変更、削除につながる可能性があります。
推奨事項¶
ユーザー入力をファイル バスの構築に使用する前に検証します。Web フレームワークに含まれるライブラリを使用して、ユーザーによって制御される値から安全にパスを構築することを強く推奨します。以下のようなライブラリがあります。
- Python の werkzeug.utils.secure_filename
- Ruby の ActiveStorage::Filenamenew(filename).sanitized
- Node の sanitize-filename
パッケージ
検証を独自に行う必要がある場合、以下を推奨します。
- 複数の "." 文字を禁止します。
- "/" や "\" などのディレクトリの区切り文字を禁止します。
- "%" などの URL エンコーディング文字を禁止します。
- ../
などの問題があるシーケンスを単純に置換するのを避けます。たとえば、.../...//
にこのフィルターを適用しても、結果の文字列は依然として ../
です。
- 可能であれば、ユーザー入力で許可される文字を [a-zA-Z0-9]+
などの限定された文字セットに制限します。
重要な点として、ユーザー入力の検証から、検証済みのユーザー入力を使用してパスを作成するまでの間で、どんな方法でもユーザー入力を処理してはいけません。そうでない場合、脆弱性が発生することがよくあります。たとえば、入力に対して URL でコードを行う別のサービスにパスが渡される場合などです。
ユーザー入力を検証した後、アプリケーションはベース ディレクトリに入力値を付加し、ファイルシステム API を使用してパスを正規化するべきです。その後、正規化されたパスが期待されるディレクトリから開始していることを検証するべきです。そうすることで、検証によって見逃されたパス トラバーサル攻撃を検出できます。
File file = new File(BASE_DIRECTORY, user_input);
if(!file.getCanonicalPath().startsWith(BASE_DIRECTORY)) {
// ... ☠️ Attempted attack. Log it, and return 400 Bad Request
}
// ... process file
サンプル¶
次のサンプルは、クエリー パラメーターにファイル名を指定することで、猫の写真をダウンロードできる API です。すぐにわかるように、API は安全に処理していません。
from fastapi import FastAPI, Response, HTTPException
import os
app = FastAPI()
@app.get("/cat-pix")
async def pix(filename: str):
base_path = '/home/jon/pix'
try:
data = open(os.path.join(base_path, filename), 'rb').read()
return Response(content=data, media_type="image/jpeg")
except FileNotFoundError:
raise HTTPException(status_code=404, detail="🙀 Cat not found")
悪意のないファイルで API を試した場合は、すべて期待どおり動作します。
$ curl "localhost:8081/cat-pix?filename=garfield.jpg"
< ... jpg bytes >
$ curl "localhost:8081/cat-pix?filename=nermal.jpg"
{"detail":"🙀 Cat not found"}
しかし、悪意のある入力によって、ファイルシステムの任意のファイルを読み取る攻撃が可能になります。
$ curl "localhost:8081/cat-pix?filename=../../../etc/passwd"
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
< ... >
何が起きたのでしょうか? コードは os.path.join("/home/jon/pix", "../../../etc/passwd")
を実行します。これは、../
があるために /etc/passwd
という結果になります。上に挙げられた推奨事項に従うと、次のようにエンドポイントを安全にできます。
@app.get("/cat-pix")
async def pix(filename: str):
base_path = '/home/jon/pix'
# Sanitize the input
filename = werkzeug.utils.secure_filename(filename)
# Compute and normalize the path
filepath = os.path.realpath(os.path.join(base_path, filename))
# Ensure that the computed path didn't escape our base path. This is for
# extra safety and should never trigger if our validation is done properly
if not filepath.startswith(base_path):
# Attack detected!
raise HTTPException(status_code=400, detail="☠️ Nice try!")
try:
data = open(filepath, 'rb').read()
except FileNotFoundError:
raise HTTPException(status_code=404, detail="🙀 Cat not found")
return Response(content=data, media_type="image/jpeg")
ファイル システムへのアクセスに使用されるパスは、既知の接頭辞に対してチェックされる前にサニタイズされ、正常化されます。これにより、どのようなユーザー入力であっても、結果のパスは安全であることが保証されます。