2026年6月1日
2026年6月1日
WordPressのデシリアライゼーション攻撃(PHPオブジェクトインジェクション)への対策
WordPressのデシリアライゼーション攻撃は、外部から送られた文字列をPHPの unserialize() で復元させ、意図しないオブジェクトを作らせることで任意の処理を実行させる攻撃です。対策の中心は「利用者が操作できる値を unserialize() / maybe_unserialize() に渡さない」ことで、データの受け渡しはJSONに置き換えます。この記事では、攻撃の仕組み、サイト運営者がやるべきこと、テーマ・プラグイン開発者が書くべきコード、自サイトのコードの点検方法を解説します。
デシリアライゼーション攻撃の仕組み
PHPの「シリアライズ」は、配列やオブジェクトを a:1:{s:4:"name";s:5:"hello";} のような文字列に変換する仕組みで、unserialize() はその逆に文字列から元のデータを復元します。問題は、文字列の中に O:8:"SomeClass":... という形でクラス名とプロパティを書けば、unserialize() がそのクラスのオブジェクトを作ってしまう点です。
オブジェクトが作られると、PHPはそのクラスに定義された特定のメソッド(マジックメソッド)を自動で呼びます。代表的なのは次のものです。
__wakeup()/__unserialize():復元された直後に呼ばれる__destruct():オブジェクトが破棄されるときに呼ばれる__toString():オブジェクトが文字列として扱われたときに呼ばれる
攻撃者は、サイトに読み込まれているクラスの中から、これらのメソッドの中でファイル書き込みやコマンド実行、データベース操作などにつながる処理を探します。プロパティの値を自由に決められるので、複数のクラスを組み合わせて最終的に危険な処理まで到達させる「POPチェーン」と呼ばれる手法が使われます。これが PHPオブジェクトインジェクション(PHP Object Injection)です。
WordPressのサイトは本体・テーマ・多数のプラグインのクラスが同時に読み込まれているため、1つのプラグインに「利用者の入力を unserialize() する」箇所があると、別のプラグインのクラスがPOPチェーンの部品として使われることがあります。脆弱性が単体のプラグインの問題に見えても、組み合わせ次第で深刻度が上がるのはこのためです。
WordPress本体での過去の事例
WordPress本体もこの種の問題に対応してきました。
- WordPress 3.6.1(2013年)では、二重シリアライズに関する脆弱性が修正されました。現在の
maybe_serialize()が、すでにシリアライズ済みに見える文字列をもう一度シリアライズするのはこの経緯によるものです。 - WordPress 6.4.2(2023年12月)では、本体単独では悪用できないものの、プラグイン側のオブジェクトインジェクションと組み合わさると任意コード実行につながる POP チェーンの問題が修正されました。
どちらも「入口(unserialize() に外部の値が届く箇所)」はプラグインやテーマ側にあることが多く、本体の修正は部品を減らす意味合いです。運営者にとっては、本体を最新に保つことと、入口を持つプラグインを早く更新することの両方が必要になります。
サイト運営者がやるべき対策
コードを書かない運営者でもできる対策は、主に「入口を早くふさぐ」「入口を減らす」「攻撃を検知する」の3つです。
本体・プラグイン・テーマを最新に保つ
オブジェクトインジェクションの脆弱性は、公表されると短期間で攻撃が始まることがあります。WordPress本体のマイナーアップデート(セキュリティ修正)は自動更新を有効のままにし、プラグインとテーマも更新通知を放置しないようにします。更新前にはバックアップを取り、ステージング環境があればそこで動作を確認してから本番に適用します。
WP-CLIが使える環境なら、更新が必要なものを一覧で確認できます。
# 更新があるプラグイン・テーマの一覧を確認する(この時点では更新しない)
wp plugin list --update=available
wp theme list --update=available
wp core check-update
使っていないプラグイン・テーマを削除する
停止中のプラグインは通常の画面からは動きませんが、ファイルが残っていると、そのクラスが別の経路で読み込まれたり、ファイルへの直接アクセスで処理が動いたりする可能性があります。使わないプラグインやテーマは停止だけでなく削除します。
脆弱性情報を追う
WPScan、Wordfence、Patchstack などが公開している脆弱性データベースでは、「PHP Object Injection」として分類された脆弱性が日常的に報告されています。自サイトで使っているプラグインの名前で定期的に確認するか、セキュリティプラグインの脆弱性通知機能を使います。
PHPのバージョンを上げる
PHP 8.0 以降では、phar:// ストリーム経由でファイルにアクセスしたときにPharのメタデータが自動でデシリアライズされる挙動がなくなりました。PHP 7系では、画像アップロードなどで細工したPharファイルを置き、ファイル関数に phar:// のパスを渡させることでデシリアライズを起こす攻撃手法がありました。サポートが続いているPHPのバージョンを使うことは、この経路の対策にもなります(対応状況は2026年10月現在、PHP公式のサポート表で確認してください)。
WAFで典型的な攻撃文字列を止める
WAF(Webアプリケーションファイアウォール)には、リクエストに O:数字:"クラス名" のようなシリアライズされたオブジェクトが含まれる場合に遮断するルールを持つものがあります。脆弱性の公表から更新までの間をしのぐ手段として有効ですが、すべての攻撃を止められるわけではないので、更新の代わりにはなりません。
開発者が書くべきコード
テーマやプラグインを自作している場合、根本的な対策は「信頼できない値を復元しない」ことです。
利用者の値は unserialize しない、JSONを使う
Cookie・フォーム・URLパラメータ・REST APIのリクエストなど、利用者が書き換えられる値に unserialize() や maybe_unserialize() を使ってはいけません。maybe_unserialize() は「シリアライズされた形式なら復元する」関数で、安全性の検査はしていない点に注意が必要です。
次は避けるべき書き方です。
// 危険な例: Cookie の値をそのまま復元している
$prefs = unserialize( base64_decode( $_COOKIE['mysite_prefs'] ) );
配列のやり取りが目的なら、JSONに置き換えます。json_decode() はオブジェクトのクラスを指定して復元する機能を持たないため、マジックメソッドが呼ばれる心配がありません。
// 安全な例: JSON で受け取り、値は許可したものだけ使う
$raw = isset( $_COOKIE['mysite_prefs'] ) ? wp_unslash( $_COOKIE['mysite_prefs'] ) : '';
$prefs = json_decode( $raw, true );
if ( ! is_array( $prefs ) ) {
$prefs = array();
}
$color = ( isset( $prefs['color'] ) && in_array( $prefs['color'], array( 'light', 'dark' ), true ) )
? $prefs['color']
: 'light';
WordPressは $_COOKIE や $_POST の値にスラッシュを付け足しているため、wp_unslash() で戻してからデコードしています。また、デコードできたあとも、使う値は許可リスト(ここでは light と dark)で検査します。
どうしても unserialize が必要なときは allowed_classes を指定する
既存データとの互換のためにシリアライズ形式を読む必要がある場合は、allowed_classes オプションでオブジェクトの復元を禁止します。
$data = unserialize( $serialized, array( 'allowed_classes' => false ) );
false を指定すると、文字列中にオブジェクトが含まれていてもクラスのオブジェクトにはならず、__PHP_Incomplete_Class として扱われるため、そのクラスのマジックメソッドは呼ばれません。ただしこれは最後の防衛線で、利用者の値をそもそも渡さない設計が先です。
外部に預けるデータには署名を付ける
Cookieや隠しフィールドに状態を持たせる必要がある場合は、HMACで署名を付け、受け取ったときに改ざんされていないか検証します。
function mysite_pack( array $data ) {
$json = wp_json_encode( $data );
$sig = hash_hmac( 'sha256', $json, wp_salt( 'auth' ) );
return base64_encode( $json ) . '.' . $sig;
}
function mysite_unpack( $packed ) {
$parts = explode( '.', (string) $packed, 2 );
if ( 2 !== count( $parts ) ) {
return null;
}
$json = base64_decode( $parts[0], true );
if ( false === $json ) {
return null;
}
$expected = hash_hmac( 'sha256', $json, wp_salt( 'auth' ) );
if ( ! hash_equals( $expected, $parts[1] ) ) {
return null;
}
$data = json_decode( $json, true );
return is_array( $data ) ? $data : null;
}
署名の比較に === ではなく hash_equals() を使うのは、比較にかかる時間の差から署名を推測される攻撃(タイミング攻撃)を避けるためです。署名の鍵には wp-config.php のソルトから作られる wp_salt() を使い、コードに秘密の文字列を直接書かないようにします。
自サイトのコードを点検する
自作テーマやプラグインに危険な箇所がないかは、unserialize を検索すると見当がつきます。
# wp-content 配下で unserialize / maybe_unserialize を使っている箇所を表示する
grep -rnE "(maybe_)?unserialize\s*\(" wp-content/themes wp-content/plugins --include="*.php"
見つかった箇所ごとに、渡している値がどこから来るかをたどります。データベースから取得した自分の設定値なら通常は問題ありませんが、$_GET・$_POST・$_COOKIE・$_REQUEST・REST APIの引数・外部APIの応答などが届く可能性があれば修正対象です。配布プラグインの中に見つかった場合は、自分で書き換えず、作者に報告するか代替プラグインを検討します。
攻撃の痕跡を探すときは、アクセスログでシリアライズ形式の文字列を検索します。URLエンコードされていることが多いので、O%3A のような形も合わせて確認します。
grep -E 'O:[0-9]+:|O%3A[0-9]+%3A' /var/log/nginx/access.log
ログの場所はサーバーによって異なります。不審なリクエストが見つかり、その後にファイルの改ざんや見覚えのない管理者ユーザーがある場合は、侵入を前提とした対応(パスワード変更、改ざんファイルの確認、バックアップからの復旧)に進んでください。
まとめ
- デシリアライゼーション攻撃は、
unserialize()に外部の値を渡させてオブジェクトを作らせ、マジックメソッド経由で危険な処理を実行させる攻撃です。 - 運営者の対策は、本体・プラグインの更新、不要なプラグインの削除、脆弱性情報の確認、PHP 8系以降への移行、WAFの併用です。
- 開発者は、利用者の値を
unserialize()/maybe_unserialize()に渡さず JSON を使い、やむを得ない場合はallowed_classes => falseを指定します。 - 外部に預ける状態は HMAC で署名し、
hash_equals()で検証します。 grepでunserializeの使用箇所を洗い出し、値の出どころを確認すると点検の手がかりになります。