皆さん、こんにちは!セキュリティバイブル主筆の〇〇です。
突然ですが、皆さんの目の前にあるPCやスマホ、毎日のように使っているアプリケーション。これらが、実はひっそりと「見えない荷物のやり取り」をしているってご存知でしたか?そして、その荷物の中身によっては、知らない間に泥棒に家の鍵を渡してしまうような、恐ろしい事態に繋がる可能性があるんです。
今回は、OWASP Top 10という「アプリケーションセキュリティの脅威ランキング」の中でも、特に開発者が見過ごしがち、しかしとてつもなく危険な脆弱性、「安全でないデシリアライゼーション」について、とことん優しく、そしてリアルな視点も交えながら解説していきます。
「デシリアライゼーション?何それ、難しそう…」って思った方も大丈夫。まるで自宅の防犯対策を学ぶように、身近な例え話を交えながら、一歩ずつ対策を学んでいきましょう!
—
宅配便の荷物と組み立て説明書:デシリアライゼーションって、そもそも何?
まず、「デシリアライゼーション」という、ちょっと聞き慣れない言葉から紐解いていきましょう。
皆さんが引っ越しをする時、大きな家具をそのまま運ぶのは大変ですよね?そこで、家具をバラバラにして箱に詰め、運びやすい状態にします。そして、新しい家に着いたら、その箱から部品を取り出し、説明書を見ながら元の家具の形に組み立て直します。
これと同じことが、コンピュータの世界でも頻繁に行われています。
シリアライゼーション:データを「運びやすい形」に梱包する作業
- プログラムの中にある「オブジェクト」と呼ばれるデータ(例えば、ユーザーの情報や設定、画像データなど、様々な情報が詰まった塊)は、そのままではネットワーク経由で送ったり、ファイルに保存したりするのに不向きな形をしています。
- そこで、これを「バイト列」という、コンピュータが扱いやすい、いわば「箱詰めされた梱包データ」に変換します。この作業を「シリアライゼーション」と呼びます。
- 例え話で言えば、家具を分解して箱に詰めるイメージですね。
デシリアライゼーション:梱包データを元の形に「組み立て直す」作業
- ネットワーク経由で送られてきたり、ファイルから読み込まれたりした「梱包データ」(バイト列)を、プログラムが元の「オブジェクト」の形に戻す作業。これが「デシリアライゼーション」です。
- 例え話で言えば、宅配便で届いた箱を開け、説明書通りに家具を組み立てる作業にあたります。
皆さんが普段使っているWebアプリケーションやサービスは、このシリアライゼーションとデシリアライゼーションを、裏側で本当に頻繁に行っているんですよ。例えば、セッション情報(ログイン状態を維持する情報)や、一時的なデータ保存、異なるシステム間のデータ連携など、様々な場面で活用されています。
宅配便の中身が爆弾だったら?「安全でないデシリアライゼーション」の怖さ
さて、ここまで聞くと、「なんだ、便利で当たり前の仕組みじゃないか」と思うかもしれません。しかし、ここに大きな落とし穴があります。
先ほどの例で考えてみましょう。もし、皆さんの家に届いた宅配便の荷物に入っていた「組み立て説明書」が、実は家具ではなく「爆弾の組み立て方」の指示書だったらどうでしょう?
プログラムは、送られてきたデータ(梱包データ)を「正しいもの」と信じ込んで、その指示書通りに、まるで思考停止したかのように「組み立て」を進めてしまいます。これが「安全でないデシリアライゼーション」の恐ろしい本質なのです。
オブジェクトインジェクション:泥棒が「偽の鍵」を送りつける手口
攻撃者は、この仕組みを悪用します。
1. アプリケーションがどんなデータをデシリアライズするかを突き止めます。
2. そして、悪意のある「組み立て指示書」(シリアライズされた悪意のあるオブジェクト)をこっそり作成します。この指示書には、例えば「このサーバーのファイル全部消去しろ!」とか、「私の言うことを聞くプログラムを動かせ!」といった、とんでもない命令が仕込まれています。
3. この悪意ある「荷物」をアプリケーションに送りつけます。
4. 何も知らないアプリケーションは、その荷物をデシリアライズ(組み立て)し、攻撃者が仕込んだ命令を、自らの手で実行してしまうのです。
これを「オブジェクトインジェクション」と呼びます。まるで、泥棒が「この鍵で家に入って、金庫を開けてください」と書かれた偽の鍵を送りつけ、家主がそれを律儀に実行してしまうようなものです。
最悪のシナリオ:リモートコード実行 (RCE) への道
このオブジェクトインジェクションが成功すると、攻撃者はアプリケーションが動作しているサーバー上で、任意のコマンドを実行できるようになります。これが「リモートコード実行 (RCE)」です。
RCEは、セキュリティの世界では「最悪のシナリオの一つ」とされています。泥棒が家に勝手に入ってきて、金庫を開けるだけでなく、家の構造を自由に変えたり、監視カメラを停止させたり、挙げ句の果てには家を乗っ取ったりするようなものです。
私たちが過去に扱ったインシデントの中にも、この「安全でないデシリアライゼーション」が原因で、たった一つのHTTPリクエストでサーバー全体が乗っ取られ、顧客情報が流出したという痛ましい事例がいくつもあります。見過ごされがちですが、これほど強力な攻撃経路になり得る脆弱性はそう多くありません。
泥棒はどうやって家に侵入する?具体的な攻撃のメカニズム
では、具体的に攻撃者はどのようなステップでこの脆弱性を悪用するのでしょうか?
1. ターゲットの特定: 攻撃者は、Webアプリケーションがどこで、どのような種類のデータをデシリアライズしているかを調べます。例えば、HTTPリクエストのクッキーやPOSTデータの隠しフィールド、ファイルのアップロード機能など、デシリアライズ処理が潜んでいそうな場所を探します。
2. ペイロードの作成: ターゲットのアプリケーションが使っているプログラミング言語(Java, PHP, Pythonなど)やライブラリに合わせて、悪意のある「梱包データ」(ペイロードと呼びます)を作成します。このペイロードは、デシリアライズされた際に、OSコマンドを実行したり、不正なファイルを作成・削除したりするような命令が仕込まれたオブジェクトを含んでいます。
- 特にJavaの世界では、「ysoserial」というツールが有名で、様々なライブラリの脆弱なデシリアライゼーションの「ガジェットチェーン」を悪用したペイロードを簡単に生成できてしまいます。
3. 攻撃の実行: 作成したペイロードを、アプリケーションがデシリアライズ処理を行うエンドポイント(URLやAPI)に送信します。
4. コードの実行: アプリケーションは、何も疑うことなくこのペイロードを受け取り、デシリアライズを開始します。その結果、悪意のあるオブジェクトが生成され、仕込まれたコードがサーバー上で実行されてしまうのです。
これは、単なるデータのやり取りに見えて、実は裏でとんでもないことが起きているんですよ。
Javaの例でイメージしてみよう
特にJavaの世界では、`java.io.ObjectInputStream`を使ったデシリアライゼーションが悪用されるケースが多く見られます。
import java.io.;
public class VulnerableDeserializationExample {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 🚨 ここが危険なポイント!
// 外部(ネットワークやファイルなど)から信頼できないデータを受け取ると想定します。
// 実際の攻撃では、攻撃者がysoserialなどで生成した悪意のあるバイト列がここに入ります。
byte[] serializedUntrustedData = getUntrustedDataFromNetwork();
// 受信したバイト列からオブジェクトをデシリアライズ
try (ByteArrayInputStream bis = new ByteArrayInputStream(serializedUntrustedData);
ObjectInputStream ois = new ObjectInputStream(bis)) {
// ここでデシリアライズが行われ、攻撃者が仕込んだオブジェクトが生成される可能性があります。
// 悪意のあるオブジェクトのreadObject()やその他のメソッドが、
// デシリアライズの過程で自動的に実行される「ガジェットチェーン」が多々存在します。
Object obj = ois.readObject();
System.out.println(“デシリアライズされたオブジェクトのクラス: ” + obj.getClass().getName());
// objに対する後続処理が危険にさらされます
System.out.println(“デシリアライズされたオブジェクト: ” + obj.toString());
} catch (InvalidClassException e) {
System.err.println(“🚨 エラー: 危険なクラスが検出されましたが、この例ではまだ防御されていません。: ” + e.getMessage());
} catch (IOException | ClassNotFoundException e) {
System.err.println(“デシリアライズ中にエラーが発生しました: ” + e.getMessage());
}
}
/
- 外部から受け取った怪しいデータをシミュレートするメソッド。
- 実際の攻撃では、RCEを狙ったガジェットチェーンを含むペイロードが送られます。
- ここでは、簡単のため、ただの文字列をシリアライズしたダミーデータを使用します。
- (このダミーデータ自体はRCEには繋がりませんが、概念を理解するために示します)
/
private static byte[] getUntrustedDataFromNetwork() {
System.out.println(“🚨 外部から受け取った怪しいデータを受信しました…”);
try {
// 例として、”Hello, malicious world!” という文字列をシリアライズしたデータ
// 実際の攻撃ペイロードはもっと複雑で、危険なクラスを含みます
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(“Hello, malicious world!”); // 攻撃者が送り込んだデータと仮定
oos.flush();
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return new byte[]{};
}
}
}
こんなコード、設定ファイルやデータベースから読み込んだデータをオブジェクトに戻す際など、どこにでもありそうですよね?だからこそ、この脆弱性は見過ごされやすく、非常に危険なんです。
泥棒から家を守る防犯対策:どうやって身を守る?
では、この危険な「安全でないデシリアライゼーション」から、皆さんのアプリケーションをどのように守れば良いのでしょうか?基本的な考え方は、「外部からのデータは基本的に信頼しない」という、セキュリティの鉄則に尽きます。
ここからは、具体的な防犯対策をいくつかご紹介しましょう。
対策1:デシリアライゼーションを避ける(究極の防御策)
可能であれば、そもそもJavaの`ObjectInputStream`のような、実行可能なオブジェクトを生成しうるデシリアライゼーションの仕組みを使わないのが究極の防御策です。
- JSONやXMLなどのデータフォーマットを利用する:
- これらのフォーマットは、基本的に純粋なデータのみを表現するために設計されており、直接的なコード実行のリスクが低いとされています。(ただし、JSONやXMLのパースライブラリ自体に脆弱性がある可能性はゼロではありませんが、デシリアライゼーションよりははるかに安全です。)
- データのみを扱い、オブジェクトの構造や挙動を再構築しないため、悪意のあるオブジェクトインジェクションのリスクを大幅に減らせます。
- 例え話: 家の鍵を閉めるだけでなく、そもそも泥棒が入れないように、家の構造自体を要塞のようにするイメージです。
対策2:信頼できるデータのみをデシリアライズする(厳重な鍵)
どうしてもデシリアライゼーションが必要な場合は、そのデータが「信頼できるソースから送られてきたものであり、途中で改ざんされていないこと」を厳重に確認する必要があります。
- デジタル署名やMAC (Message Authentication Code) の利用:
- デシリアライズするデータにデジタル署名やMACを付与し、受信側でその正当性を検証します。
- これにより、データが信頼できる送信者によって作成され、かつ途中で改ざんされていないことを確認できます。
- 例え話: 宅配便の送り状に、本物であることを示す特殊なスタンプや封印が押されているか確認し、それが偽造されていないかを厳しくチェックするようなものです。
対策3:デシリアライズするオブジェクトの型を制限する(怪しい荷物は開けない)
これが最も現実的で効果的な防御策の一つです。アプリケーションがデシリアライズを許可するクラスを、あらかじめ「ホワイトリスト」として明示的に指定します。ホワイトリストにないクラスのデシリアライズは拒否する、という方法です。
Javaの場合、`ObjectInputStream`を継承したカスタムクラスを作成し、`resolveClass`メソッドをオーバーライドすることで、この型チェックを実装できます。
import java.io.;
import java.util.HashSet;
import java.util.Set;
public class SafeDeserializationExample {
// 🚨 ここが防御の肝!デシリアライズを許可するクラス名のホワイトリスト
private static final Set
static {
// 例: 自社の安全なデータクラスや、標準ライブラリの安全なクラスのみを許可
ALLOWED_CLASSES.add(“com.example.myapp.data.MySafeObject”); // アプリケーションで定義された安全なオブジェクト
ALLOWED_CLASSES.add(“java.lang.String”); // Stringクラスはよく使われるので許可
ALLOWED_CLASSES.add(“java.util.ArrayList”); // ArrayListなど、一般的なコレクションも必要に応じて
// 必要最低限のクラスのみを許可するように厳しく設定しましょう
}
public static void main(String[] args) {
// 外部(ネットワークやファイルなど)から信頼できないデータを受け取ると想定します。
byte[] serializedUntrustedData = getUntrustedDataFromNetwork();
try (ByteArrayInputStream bis = new ByteArrayInputStream(serializedUntrustedData);
SafeObjectInputStream ois = new SafeObjectInputStream(bis)) { // 🚨 カスタムのSafeObjectInputStreamを使用
Object obj = ois.readObject();
System.out.println(“✅ 安全にデシリアライズされました: ” + obj.getClass().getName());
System.out.println(“デシリアライズされたオブジェクト: ” + obj.toString());
} catch (InvalidClassException e) {
// 許可されていないクラスが検出された場合
System.err.println(“🚨 デシリアライズを拒否しました!許可されていない危険なクラスが検出されました: ” + e.getMessage());
} catch (IOException | ClassNotFoundException e) {
System.err.println(“デシリアライズ中にエラーが発生しました: ” + e.getMessage());
e.printStackTrace();
}
}
/
- ObjectInputStreamを継承し、resolveClassメソッドをオーバーライドして型チェックを行うクラス。
- これにより、ホワイトリストにないクラスのデシリアライズをブロックできます。
/
static class SafeObjectInputStream extends ObjectInputStream {
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
// デシリアライズしようとしているクラス名を取得
String className = desc.getName();
// 🚨 ホワイトリストに含まれないクラスであれば、例外をスローしてデシリアライズを拒否
if (!ALLOWED_CLASSES.contains(className)) {
throw new InvalidClassException(“デシリアライズが許可されていないクラス: ” + className);
}
// 許可されたクラスであれば、親クラスのresolveClassを呼び出して通常の処理を続行
return super.resolveClass(desc);
}
}
/
- 外部から受け取った怪しいデータをシミュレートするメソッド。
- ここでは、ホワイトリスト外の架空の危険なクラス “com.example.malicious.EvilObject” を
- シリアライズしたデータが送られてきたと仮定します。
/
private static byte[] getUntrustedDataFromNetwork() {
System.out.println(“🚨 外部から受け取った怪しいデータを受信しました…”);
try {
// 例として、”com.example.malicious.EvilObject” をシリアライズしたダミーデータ
// 実際にはもっと複雑なバイト列になりますが、概念を示すためのものです。
// これをデシリアライズしようとすると、SafeObjectInputStreamでブロックされるはずです。
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
// 実際にはEvilObjectクラスが存在しないので、ここではStringをシリアライズし、
// 実行時にクラス名がEvilObjectとみなされるようなペイロードを想定します。
// より現実的な例にするため、ここでは実際にホワイトリスト外のクラス(MyUnsafeObject)を用意します。
oos.writeObject(new MyUnsafeObject(“I am dangerous!”)); // 許可されていないオブジェクトを送り込んだと仮定
oos.flush();
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return new byte[]{};
}
}
// 許可するクラスの例(Serializableを実装している必要がある)
static class MySafeObject implements Serializable {
private static final long serialVersionUID = 1L; // シリアライズバージョンUID
private String data;
public MySafeObject(String data) { this.data = data; }
public String getData() { return data; }
@Override
public String toString() { return “MySafeObject{data='” + data + “‘}”; }
}
// 許可しないクラスの例(Serializableを実装している必要がある)
static class MyUnsafeObject implements Serializable {
private static final long serialVersionUID = 1L; // シリアライズバージョンUID
private String evilMessage;
public MyUnsafeObject(String msg) { this.evilMessage = msg; }
public String getEvilMessage() { return evilMessage; }
@Override
public String toString() { return “MyUnsafeObject{evilMessage='” + evilMessage + “‘}”; }
}
}
このコードでは、`SafeObjectInputStream`という独自のクラスを使うことで、`ALLOWED_CLASSES`に定義されたクラス以外のデシリアライズを拒否しています。これにより、攻撃者がどんなに悪意のあるオブジェクトを送りつけようとしても、ホワイトリストにない限り、アプリケーションはそれを「組み立て」ることを拒否できるわけです。
- 例え話: 宅配便が届いても、送り状に書かれた送り主や品名が、あらかじめ決めておいた「信頼できるリスト」にない場合、一切受け取らないという防犯対策です。
対策4:デシリアライゼーションのモニタリングとログ(不審者情報の共有)
万が一、攻撃が試みられた場合に備えて、デシリアライゼーション処理のログを詳細に記録し、異常なアクセスパターンやエラーが発生した場合には、即座にアラートを出す仕組みを構築することも重要です。
- 例え話: 家の周りに防犯カメラを設置して、不審な動きがないか常に監視し、何かあればすぐに警備会社に連絡がいくようにするイメージです。実際に攻撃が成功してしまった場合でも、ログが残っていれば、被害範囲の特定や復旧作業に役立ちます。
まとめ:あなたのアプリを安全な家にするために
「安全でないデシリアライゼーション」は、一見地味に見えるかもしれませんが、一度悪用されると、サーバー乗っ取りという最悪の事態に直結する非常に強力な攻撃経路です。特に、古くから使われているライブラリやフレームワークでは、気づかないうちにこの脆弱性が潜んでいることも珍しくありません。
一番大切なのは、「外部からのデータは基本的に信頼しない」というセキュリティの基本原則を常に心に留めておくこと。
そして、以下の点を再確認してみてください。
- 本当にデシリアライゼーションが必要ですか? データ形式の変更を検討しましょう。
- デシリアライズするデータの信頼性は確保されていますか? 署名やMACで検証しましょう。
- デシリアライズするオブジェクトの型は制限されていますか? ホワイトリスト方式を導入しましょう。
- デシリアライゼーションの状況は監視されていますか? ログとアラートを整備しましょう。
皆さんのアプリケーションが、攻撃者から堅固に守られた「安全な家」になるように、一歩ずつ対策を学んでいきましょう!
OWASP Top 10には、他にも様々な脅威がリストアップされています。一つずつ理解を深めていくことで、皆さんの開発スキルだけでなく、セキュリティに対する意識もきっと高まるはずです。
また次回、別のセキュリティテーマでお会いしましょう!

コメント