信頼できないデータの逆シリアル化

信頼できないデータの逆シリアル化

説明

逆シリアル化は、保存・送信されたデータからアプリケーションのオブジェクトを復元する処理です。Pythonの各形式は、セキュリティ上の性質が異なります。

  • pickle.load、pickle.loads、Unpickler.load() は、復元中に関数やクラスを検索して呼び出すことがあります。悪意あるpickleデータは読み込み中に任意のコードを実行できるため、信頼できるデータだけを扱ってください。
  • PyYAMLの yaml.unsafe_load、Loader、CLoader、UnsafeLoader、CUnsafeLoader は、Pythonオブジェクトを生成するタグを許可します。信頼できないYAMLには使用しないでください。
  • marshal は主にPythonの .pyc 用の内部形式で、悪意あるデータに対して安全ではありません。allow_code=True ではコードオブジェクトを返せますが、復元処理自体がそのコードを実行するわけではありません。破損した入力、互換性のないバージョン、復元後の処理によって、例外、未定義の動作、その他のセキュリティ上の問題が起こるおそれがあります。

pickleや安全でないYAMLローダーのコード実行リスクと、marshal の完全性・可用性・互換性の問題は区別する必要があります。

想定される影響

  • コード実行: 悪意あるpickleデータや安全でないYAMLタグにより、アプリケーションの権限で関数やコマンドを実行されるおそれがあります。
  • 状態の改ざん: 想定外のオブジェクトや値によって、アプリケーションの状態や保存データを変更される可能性があります。
  • 情報漏えい: 実行されたコードや後続処理が、ファイル、秘密情報、環境情報にアクセスするおそれがあります。
  • サービス拒否: 悪意ある入力や過大な入力が、CPU、メモリ、再帰の上限を使い切ったり、例外を繰り返し発生させたりする可能性があります。
  • 互換性の問題: marshal のコードオブジェクトはPythonのバージョン間で互換性がなく、異なるバージョンで復元すると未定義の動作になる場合があります。

シリアライズ形式の選択

信頼できない入力には、JSONや yaml.safe_load のようにデータだけを扱う形式を優先します。Unpickler を変数に保存して使ったり、独自のローダーを用いたりしても、入力の信頼性を確認する必要は変わりません。

対処方法

  1. 外部入力を扱う経路から pickle と marshal を取り除き、JSONなどのデータ専用形式に置き換えます。
  2. 入力サイズを制限し、解析結果の型、必須フィールド、値の型・長さ・範囲を検証します。スキーマ検証はデータ専用パーサーの後に行います。安全でない逆シリアル化の後で検証しても、読み込み中のコード実行は防げません。
  3. 信頼できないYAMLには yaml.safe_load または yaml.load(..., Loader=yaml.SafeLoader) を使います。利用できる場合は、CSafeLoader も同じ制限されたコンストラクターを使います。
  4. 既存のオブジェクト形式をすぐに廃止できない場合は、信頼できる認証済みの主体だけがデータを作れるようにし、逆シリアル化の前に受け取ったバイト列の真正性と完全性を検証します。HMACは鍵を持たない攻撃者の改ざんを防げますが、正規の作成者が悪意を持って作ったデータを安全にはしません。
  5. marshal.loads(..., allow_code=False)、HTTPの Content-Type、認証・認可、逆シリアル化後の検証、許可範囲の広い独自の Unpickler だけに頼らないでください。
  6. 可用性を守るためにサイズ・時間・メモリの上限も設けます。ただし、信頼できない入力を扱う経路から危険な逆シリアル化処理を取り除く対策の代わりにはなりません。

例

変更前

python
import pickle

from flask import Flask, request

app = Flask(__name__)


@app.post("/profile/import")
def import_profile():
    raw = request.get_data(cache=False)
    profile = pickle.loads(raw)  # 悪意あるpickleデータは、この呼び出し中にコードを実行できます
    return {"name": str(profile.get("name", ""))}

リクエスト本文の検証より先に pickle.loads が実行されます。返されたオブジェクトを検証しても、読み込み中に実行されたコードの影響は取り消せません。

変更後

外部APIをJSONだけ受け付ける仕様に変更し、解析後にスキーマを検証します。

python
from flask import Flask, abort, request

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 64 * 1024


@app.post("/profile/import")
def import_profile():
    profile = request.get_json()
    if not isinstance(profile, dict):
        abort(400, "profile must be an object")

    name = profile.get("name")
    if not isinstance(name, str) or not 1 <= len(name) <= 100:
        abort(400, "invalid name")

    return {"name": name}

YAMLが必要な場合は、安全なローダーで基本的な値だけを生成し、その後でスキーマを検証します。

python
import yaml
from flask import abort, request


def read_yaml_profile():
    profile = yaml.safe_load(request.get_data(cache=False))
    if not isinstance(profile, dict):
        abort(400, "profile must be a mapping")
    return profile

yaml.safe_load は、入力サイズやアプリケーションのスキーマまで検証するものではありません。サイズ制限と解析後の検証も適用してください。

参考資料