パストラバーサル

ユーザー入力のパスが、意図した基準ディレクトリ外のファイルに到達しないように制限してください。

説明

パストラバーサルは、入力によってファイルシステムのパスを操作し、本来の対象ファイルやディレクトリの範囲外へアクセスする攻撃です。攻撃者は../などの相対パスを使い、システムの機密ファイルにアクセスする可能性があります。

想定される影響

  • 情報漏えい: 攻撃者がシステムの機密ファイルを読み、重要な情報を得る可能性があります。
  • データの破損: 攻撃者がシステムファイルを上書き・削除するおそれがあります。
  • 権限昇格: 重要なファイルの変更によって、攻撃者がより強い権限を得る可能性があります。

対処方法

  • ファイル名とパスを検証してください。../という文字列の拒否だけに頼らず、実際のアクセス先が許可したディレクトリ内にあることを確認してください。
  • 許可リストで指定したディレクトリ内のファイルだけにアクセスを制限してください。
  • Flask/Werkzeugでは、secure_filename()、safe_join()、send_from_directory()など、パストラバーサル対策を考慮したAPIを優先してください。
  • ファイル名だけが必要なら、os.path.basename()やpathlib.PurePath(...).nameで最後の構成要素を取り出し、必ず信頼できる基準ディレクトリと組み合わせてください。

例

open

変更前

python
# Flaskの変更前の例
from flask import Flask, request

app = Flask(__name__)

@app.route('/read')
def unsafe_read():
    filename = request.args.get('filename')
    with open(filename, 'r') as file:
        content = file.read()
    return content

変更後

python
# Flaskの変更後の例
from flask import Flask, request, abort
from pathlib import Path
from werkzeug.security import safe_join
from werkzeug.utils import secure_filename

app = Flask(__name__)
# 攻撃者が変更できないサーバー管理のディレクトリ
BASE_DIR = Path("/safe/directory").resolve(strict=True)

@app.route('/read')
def safe_read():
    filename = secure_filename(request.args.get('filename', ''))
    if not filename:
        return abort(400, description="Invalid file path")

    candidate = safe_join(str(BASE_DIR), filename)
    if candidate is None:
        return abort(400, description="Invalid file path")

    try:
        fullpath = Path(candidate).resolve(strict=True)
        fullpath.relative_to(BASE_DIR)
    except FileNotFoundError:
        return abort(404, description="File not found")
    except ValueError:
        return abort(400, description="Invalid file path")

    if not fullpath.is_file():
        return abort(404, description="File not found")
    with open(fullpath, 'r') as file:
        content = file.read()
    return content

説明:

  • 変更前: 未検証の入力をパスに使うため、攻撃者が../などの相対パスで機密ファイルへアクセスできます。
  • 変更後: 空の名前を拒否し、secure_filename()とsafe_join()でパスの構成要素を制限します。さらにPath.resolve(strict=True)でシンボリックリンクを解決し、実際のファイルが攻撃者の変更できない基準ディレクトリ内にあることを確認します。

ファイル名の構成要素だけを使う例

python
import os
from pathlib import PurePath

def filename_only(raw_name):
    if not isinstance(raw_name, str):
        raise TypeError("filename must be a string")
    # どちらかの方法でディレクトリ部分を取り除く
    name = os.path.basename(raw_name)
    # name = PurePath(raw_name).name
    if name in {"", ".", ".."}:
        raise ValueError("invalid filename")
    return name

basenameとPurePath.nameは、パスの最後の構成要素だけを取り出します。ファイルを開く場合や送信する場合は、この値も信頼できる基準ディレクトリと組み合わせてください。

Flask send_file

変更前

python
# Unsafe Flask send_file
from flask import Flask, request, send_file

app = Flask(__name__)

@app.route('/read')
def unsafe_read():
    filename = request.args.get('filename')
    return send_file(filename)

変更後

python
# Safe Flask send_from_directory
from flask import Flask, request, send_from_directory
from pathlib import Path
from werkzeug.utils import secure_filename

app = Flask(__name__)
BASE_DIR = Path("/safe/directory").resolve(strict=True)

@app.route('/read')
def safe_read():
    filename = secure_filename(request.args.get('filename', ''))
    if not filename:
        return "Invalid file path", 400
    return send_from_directory(BASE_DIR, filename, as_attachment=True)

説明:

  • 変更前: 未検証の入力をパスに使うため、攻撃者が../などの相対パスで機密ファイルへアクセスできます。
  • 変更後: send_from_directoryで指定ディレクトリ内に制限し、空の名前を拒否してからsecure_filename()でファイル名を制限します。攻撃者がファイルやシンボリックリンクを追加できないよう、基準ディレクトリをサーバー側で管理する必要があります。

参考資料