Skip to main content

Python pickle完全ガイド

著者
Headshot of Thomas Daniels

Thomas Daniels

blog hero snyk python security

2022年4月20日

0 分で読めます

アプリケーションの開発では、異なる実行環境で利用するために、オブジェクトなどの複雑なデータを永続化する必要がよくあります。しかし、複雑なデータ構造やオブジェクトの永続化は簡単ではありません。Pythonでは、組み込みのpickleライブラリを使ってこの処理を行えます。pickleは、Pythonオブジェクトをフラットなバイトストリームに変換(pickle化)し、バイトストリームをPythonオブジェクトに戻す(pickle解除)ことができます。

複雑なデータを永続化するには、ディスクに保存したりネットワーク経由で送信したりできるよう、フラットなバイトストリームとして表現する必要があります。オブジェクト構造をバイトストリームに変換する処理は、マーシャリングまたはシリアライズと呼ばれます。アプリケーションがバイトストリームを読み込んだり受信したりすると、逆の処理を行い、オブジェクト構造に変換します。これはアンマーシャリングまたはデシリアライズと呼ばれます。

注:pickleはPython専用のため、Pythonアプリケーションなら簡単に使えます。一方で、異なる言語で書かれたアプリケーション間のデータ交換には使用できません。

この記事では、アプリケーションでpickleを安全に使用する方法を解説します。pickleはさまざまな場面で非常に便利ですが、信頼できるデータだけを必ずpickle解除してください。信頼できないデータをpickle解除すると、任意のコードが実行されるおそれがあり、重大なセキュリティ脆弱性の一般的な原因となります。

無料のオンラインコードチェッカーのプロモーションバナー。Pythonコード、「コードをチェック」ボタン、Snykのブランドロゴが表示されています

Python pickleの使い方

pickleの使い方は簡単です。さっそく見ていきましょう。pickle化とpickle解除に必要な関数はすべてpickleモジュールに含まれています。まずはこのモジュールをインポートします。

import pickle

Pythonオブジェクトをpickle化する際は、ファイルに直接保存する方法と、後でコード内で使えるbytesオブジェクトに変換する方法があります。どちらの場合も、メソッドを1回呼び出すだけです。

オブジェクトをファイルにpickle化するには、pickle.dump(object, file)を呼び出します。pickle化したバイトデータだけを取得するには、pickle.dumps(object)を呼び出します。

前述のとおり、pickleはPythonで簡単に使えます。次のような多くのデータ型をpickle化できます。

  • Pythonのプリミティブ型

  • タプル、リスト、セット、辞書などのネストされたコレクション構造(辞書にpickle化可能なオブジェクトだけが含まれている場合)

  • ほとんどのクラスインスタンス

pickle化できるものについては、Pythonのドキュメントに詳細な一覧があります。

pickle化できないオブジェクトをpickle化しようとすると、PythonはPicklingErrorを発生させます。

Python pickleの使用例

実際にpickle化してみましょう。以下は、架空のゲームの状態を表すデータ構造です。このゲームには、座標のタプルで表される特定の場所に立つプレイヤーがいます。また、ワールド内にはタプルで表される位置に障害物があります。プレイヤーは、名前と価格を持つアイテムを所有しています。クラスは次のようになります。

class GameItem:
    def __init__(self, name, cost):
        self.name = name
        self.cost = cost

class GameState:
    def __init__(self, player_coordinates, obstacles, items):
        self.player = player_coordinates # tuple (x, y)
        self.obstacles = obstacles # set of tuples (x, y)
        self.items = items # list of GameItems

では、具体的な状態を作成して、ファイルにpickle化してみましょう。

player = (3, 2)
obstacles = { (1, 1), (5, 6), (7, 4), (0, -1) }
items = [ GameItem("Sword", 500), GameItem("Potion", 150) ]

state = GameState(player, obstacles, items)
with open("state.bin", "wb") as f: # "wb" because we want to write in binary mode
    pickle.dump(state, f)

pickleは人間には読めないバイナリ形式です。そのため、新しく作成したstate.binファイルの内容を調べても、ほとんど意味が分からないでしょう。GameStateやobstaclesなど、一部の識別子名を見つけられる程度です。

ゲームの状態をファイルに保存できたので、そのファイルから状態を読み込んで、ゲームにセーブ・再開機能を追加してみましょう。pickle.dumpとpickle.dumpsには、それぞれpickle解除に対応する関数があります。pickle.load(_file_)はファイルからpickle化されたPythonオブジェクトを読み込み、pickle.loads(_bytes_)は指定されたバイトデータから同じ処理を行います。

with open("state.bin", "rb") as f: # "rb" because we want to read in binary mode
    state = pickle.load(f)

print("Player coordinates:", state.player)
print("Obstacles:", state.obstacles)
print("Number of items:", len(state.items))

ご覧のとおり、pickle化の基本はとても簡単です。Pythonオブジェクトの保存も読み込みも、メソッドを呼び出すだけでできます。

Python pickleを悪用する方法

導入で説明したとおり、信頼できるデータだけを必ずpickle解除してください。pickleモジュールは安全ではありません。攻撃者が用意したバイトストリームをpickle解除すると、任意のコードが実行される可能性があります。これはpickle形式が非常に強力であることの副作用です。

Pythonオブジェクトは、特殊な_reduce_メソッドを使って、どのようにpickle化するかを指定できます。このメソッドは文字列またはタプルを返します。文字列はグローバル変数の名前を表します。タプルは、呼び出し可能なコード(関数やクラスなど)、そのコードに渡す引数、およびこの例では関係のない任意の情報を表します。pickle解除の際には、指定された呼び出し可能なコードが引数とともに実行されます。

この仕組みを利用すれば、pickle解除時に実行したい任意の関数を呼び出すpickle化オブジェクトを作成できます。たとえば、pickle解除時にos.system関数を実行してシステムコマンドを実行したり、evalを使って任意のPythonコードを実行したりできます。

eval関数を使った、この攻撃の(無害な)例を紹介します。読み込み時にprint関数を呼び出すだけです。

import pickle

class Attack:
    def __reduce__(self):
        return (eval, ("print(1+2)",))

malicious = pickle.dumps(Attack())

pickle.loads(malicious)

このコードを実行すると、データのpickle解除時にeval("print(1+2)")が実行されるため、コンソールに数値3が出力されます。

Python pickleを安全に使う方法

信頼できないクライアントからデータを受け取る必要がある場合、前述のリスクがあるためpickleは使用できません。代わりに、JavaScript Object Notation(JSON)など、別のデータシリアライズ形式を使用してください。

この処理にはPythonのjsonモジュールを使用できます。ただし、jsonモジュールはpickleほど高機能ではありません。カスタムオブジェクトなどの複雑なデータ型を標準ではサポートしていないためです。また、JSONが扱えるネイティブデータ型は限られています。たとえば、Pythonのセットに相当する型はJSONにはないため、セットにはカスタムエンコーダーが必要です。それでも、信頼できないデータを扱う場合、JSONは安全な形式です。

信頼できるアプリケーションがpickle化データを生成しても、pickle化してからpickle解除するまでの間、その完全性を保証できないケースを考えてみましょう。安全でないネットワーク経由でpickle化データを送信したり、攻撃者がアクセスできる可能性のある永続ストレージに保存したりする場合など、データを信頼できないことがあります。

いずれの場合も、HMACを使ってpickle化データの暗号学的署名を生成する方法があります。署名とともにデータを送信または保存します。pickle解除する前に、受信側で署名を検証すれば、データの完全性を確認できます。

次のように署名を生成できます(SHA256を使用)。

SECRET_KEY = b"your secret key here"
obj = [ "test", (1, 2), [ "a", "b" ] ]
data = pickle.dumps(obj)
digest = hmac.new(SECRET_KEY, data, hashlib.sha256).hexdigest()

注意:実際の環境では、シークレットキーは、信頼できない環境に決して公開されないサーバーサイドアプリケーション内で安全に保管します。コードベースにシークレットキーをハードコードしないでください。=)

受信側では、pickle化データの予想ダイジェストを計算し、受け取ったダイジェストと一致するか確認できます。

expected_digest = hmac.new(SECRET_KEY, data, hashlib.sha256).hexdigest()
if expected_digest != digest:
    print("Data integrity violated")
else:
    unpickled = pickle.loads(data)
    print(unpickled)

この方法なら、攻撃者がpickle化データを改ざんしても、シークレットキーを持っていなければ署名を改ざんしたり、受信側に信頼できないコードを実行させたりすることはできません。

まとめ

これで、Pythonのpickleモジュールを使って、複雑なPythonオブジェクトを安全にシリアライズおよびデシリアライズする方法が分かりました。これは大きな前進です。

pickleはコードに重大なセキュリティ脆弱性をもたらす可能性があるため、信頼できないデータをpickle解除してはいけません。信頼できないクライアントからデータを受け取る必要がある場合は、より安全なJSON形式を使用してください。また、信頼できるアプリケーション間でpickle化データを転送する際、改ざん防止を強化したい場合は、pickle解除前に検証できるHMAC署名を生成してください。

キャプチャー・ザ・フラッグを始めよう

オンデマンドのバーチャル入門ワークショップを視聴して、キャプチャー・ザ・フラッグの課題の解き方を学びましょう。