FlawOpen

flawopen.com/パストラバーサル/Python os.path.join()

Python の os.path.join() はパストラバーサルを防げますか?

CWE-22: Path Traversalリファレンスページ
要点

いいえ。ベースを部屋とすると、.. は親の部屋を通って外へ進み、絶対パスは新しい住所なので os.path.join() は最初の部屋を破棄します。開く前に最終パスを解決し、許可ディレクトリ内にあるか確認してください。

VULNERABLE
import os

base_dir = "/srv/uploads"
user_filename = "../../outside/demo.txt"
path = os.path.join(base_dir, user_filename)
# Relative traversal can leave the base.

path = os.path.join(base_dir, "/outside/demo.txt")
# An absolute segment discards base_dir.
SECURE — resolve and check containment
import os

def resolve_under_base(base_dir, user_filename):
    if not isinstance(user_filename, str) or not user_filename:
        raise ValueError("Invalid filename")

    base = os.path.realpath(base_dir)
    candidate = os.path.realpath(
        os.path.join(base, user_filename)
    )

    try:
        inside = os.path.commonpath((base, candidate)) == base
    except ValueError:  # e.g. different Windows drives
        inside = False

    if not inside or candidate == base:
        raise ValueError("Invalid filename")
    return candidate

考慮すべき2つのパス結合動作

.. は相対パスを想定外のディレクトリへ導きます。Python の説明では、絶対パス要素があると os.path.join() は前の要素を破棄します。Windows のドライブとルート規則は POSIX と異なります。pathlib.Path(base) / user_input も同様です。結合や正規化は認可ではありません。

commonpath() が文字列 prefix より安全な理由

比較前に信頼できるベースと候補を解決します。os.path.commonpath() は文字列ではなくパス要素を比較し、互換性のないルートやドライブで ValueError を送出するため拒否します。realpath() は検査時点のリンクを解決しますが、その後の変更は防ぎません。ツリーを変更できる第三者がいる場合は適切なディスクリプターアクセスと no-follow を使います。

よくある質問

pathlib も同じ動作ですか?

はい。Path の / は次のパスが絶対パスなら前の要素を破棄します。結果を解決し、許可ベース内か確認します。

realpath() で競合状態を防げますか?

いいえ。検証時点のリンクを解決するだけで、別プロセスがファイルを開く前に変更できます。書き込みを制限するか、対応環境では no-follow のディスクリプター相対操作を使います。

参考資料

関連項目 パストラバーサル(CWE-22)の仕組みと各言語での防ぎ方 — flawopen.com Zip Slip 脆弱性とは? Node.js path.join() では Path Traversal を防げない PHP include() とユーザー制御パス