nonceで防いだ。でも、本当に安全?
この記事は以下の動画の解説記事です。
前回は、nonceなしのGETパラメータを使ったCSRF攻撃のデモを見ました。管理者がリンクを踏むだけで設定が書き換わる、というものです。
今回は修正版プラグインのコードを確認して、実際に攻撃がブロックされることを検証します。ただ、それだけでは終わりません。「nonceを実装したから安全」と思いきや、別の問題が潜んでいます。
修正版のコードを確認する
修正版のプラグインでは、設定フォームに `wp_nonce_field()` が追加されています。
wp_nonce_field(DEMO_NOTICE_BAR_SECURE_PREFIX . 'submit_notice');引数に注目してください。プラグインのプレフィックス定数に、submitボタンのname属性(`submit_notice`)を連結しています。これがnonceのアクション名になります。
プレフィックスを使うことで、プラグイン間での名前の衝突を防げます。また、アクション名を別途考える手間も省けます。私がUdemyのWordPressプラグイン開発講座でも紹介している、自己流のパターンです。
サーバーが画面を生成するとき、このトークンも一緒に発行されます。フォームの外からはこのトークンを知る方法がないため、外部から細工したURLで同じリクエストを再現することができなくなります。

処理側の `process_get()` には `check_admin_referer()` が追加されました。引数には、先ほどと同じ式を渡しています。生成と検証で同じアクション名を使うことが必要です。

check_admin_referer(DEMO_NOTICE_BAR_SECURE_PREFIX . 'submit_notice');この関数は、リクエストにnonceが含まれているか、そして有効かを検証します。検証に失敗すると `wp_die()` が呼ばれて処理が止まります。
攻撃がブロックされることを確認
前回と同じ攻撃URLを使ってみます。ブラウザに貼り付けてアクセスすると——WordPressのエラー画面が表示されました。「このページにはアクセスできません」という内容です。`wp_die()` が動いた証拠です。

設定ページを確認してみると、前回は即座に書き換わっていた通知バーの内容が変化していません。nonceのないリクエストは完全にブロックされています。
念のため、正規のフォームから操作してみます。設定ページを開いて値を入力して保存すると——こちらは問題なく保存されました。正規のフォームはnonceを持っているので、正常に通ります。
攻撃はブロック、正規操作は通過。意図どおりの動きです。
「これで安全?」という問い
さて、nonceを実装したことでCSRF攻撃は防げました。
では、このプラグインはこれで完全に安全になったでしょうか。
このプラグインは、設定した文字列をフロントエンドに表示します。表示するとき、その文字列をそのままHTMLに書き出しています。保存した値が、HTMLの中に直接出力されるわけです。
ここで試したいことがあります。設定フォームに、こんな値を入力して保存してみます。
<script>alert(`XSS`)</script>nonceが実装されているので、正規のフォームから保存できます。フロントエンドを開くと——アラートが表示されました。

JavaScriptが実行されています。このサイトを訪れた人全員に、このスクリプトが実行されます。
もう少し踏み込んでみます。別のURLに飛ばすコードを保存してフロントを開くと、別のサイトにリダイレクトされました。

さらに無限ループのコードを試すと、アラートが止まらなくなりブラウザが操作不能になります。実際の攻撃では、アラートの代わりにクッキー情報の抜き取りや、管理者セッションの乗っ取りコードが入ります。
なぜnonceがあってもスクリプトが動くのか
nonceの検証は、リクエストの正当性を確認する仕組みです。「本人の意図で送られたリクエストか」を見ています。
一方、今回起きたことはXSSです。保存された値が出力されるときの問題です。「保存された文字列を、安全にHTMLとして出力できているか」という、まったく別の問題です。
この2つは独立しています。nonceを実装しても、出力エスケープがなければXSSは防げません。
入口(リクエスト)を守ることと、出口(出力)を守ることは、別々に対処する必要があります。
動画でも同じ内容をデモ形式で解説しています。
まとめと次回予告
今回のポイントです。
nonceはリクエストの正当性を守る。出力の安全性とは別の問題
保存された値をそのまま出力すると、スクリプトが実行される(XSS)
入口の守りと出口の守りは、それぞれ別に実装する必要がある
次回のEpisode 02では、このXSSをテーマにします。なぜ起きるのか、どう防ぐのか。サニタイズ・エスケープ・バリデーションの違いも整理しながら、コードで確認していきます。
このシリーズは「壊して学ぶ WordPress Security」です。ローカル環境での安全な検証を通じて、WordPressプラグインの脆弱性と安全な実装の違いを学んでいきます。
