PHPでメールフォームを作成したので、脆弱性がないかアドバイスいただけないでしょうか。どれどれ…と確認すると、トークンのチェックが入っているにも関わらずクロスサイト・リクエストフォージェリ(CSRF)脆弱性が残存しています。このため、PoC(Proof of Concept)を回答し、CSRF脆弱性があることを指摘しましたが、質問者の学習機会を奪わないためにわざと解説は省きました。本稿では、この脆弱性のメカニズムについて報告します。
エンジニアでもなければ、PHPもろくに書けない雑魚ですが、「php メールフォーム 作り方」でググって表示されるサイトを見ると、「んんんんん???」と思うところがあります。
これらを参考にしたり、コピペする方は、記述されているコードの良し悪しは判断できないかと思います。
そのような方々が参考にできるメールフォームを作りたいという思いで、調べて作りました。
周りに書いたコードを確認してもらえる人もいないので、皆様からのアドバイスがほしいです((_ _ (´ω` )ペコ
【PHP】作成したメールフォームに脆弱性がないか、アドバイスもらえないでしょうかより引用
処理概要
このメールフォームは、元記事に以下のように書かれているように、典型的な入力・確認・送信の3ステップからなります。入力(index.php) → 確認(confirm.php) → 送信(send.php) と画面を遷移してメールを送ります。以下、各ステップの処理概要を示します。
入力(index.php)
- セッションを開始し、トークンを$_SESSION['token']に格納する
- 入力フォームを表示する
- hiddenパラメータtokenにてトークンをPOSTする
確認(confirm.php)
- トークン$_SESSION['token'] がNULLの場合、空文字列に置き換える
- POSTされたトークンが$_SESSION['token']に一致しなければエラー表示
- 入力値をバリデーションし、エラーがなければセッション変数にセットする
- hiddenパラメータtokenにてトークンをPOSTする
送信(send.php)
- トークン$_SESSION['token'] がNULLの場合、空文字列に置き換える
- POSTされたトークンが$_SESSION['token']に一致しなければエラー表示
- メールアドレスや問い合わせ内容等をセッション変数から取り出す
- 問い合わせをしたユーザと管理者にメール送信
続いて、CSRF攻撃について検討します。
脅威1: いきなり send.php に外部からPOSTされる
まずは、いきなり送信ページ(send.php)に対してPOSTする場合を考えましょう。この場合、セッション変数は初期化されていないわけですから、$_SESSION['token]はNULLという扱いになりますが、前述のように、この場合$_SESSION['token']は空文字列が入ります。したがって、POSTするトークンとして空文字列を送ってやれば、CSRF対策を回避できます。しかしながら、問い合わせしたユーザのメールアドレスや問い合わせ内容はセッション変数経由で受け渡しをしているため、いずれもNULLとなります。このためメールも送信されない…わけではなく、管理者向けに内容が空(テンプレートのみ)のメールが送信されます。すごい実害があるわけではありませんが、嫌がらせ程度には使えるかもしれません。
ともあれ、空文字列のトークンでCSRF対策が回避できることは、大きな問題です。
脅威2: confirm.php、send.php に続けてPOSTされる
次の攻撃パターンは、まずconfirm.phpに外部からPOSTし、少し時間をおいてsend.phpにPOSTする攻撃です。この場合もセッション変数は初期化されていないので、$_SESSION['token]はNULLという扱いになり、send.phpと同じ流れで $_SESSION['token']は空文字列が入ります。したがって、POSTするトークンとして空文字列を送ってやれば、CSRF対策を回避できます。
加えて、問い合わせ内容を攻撃の趣旨に合わせてPOSTすれば、バリデーションを経てセッション変数にセットされます。
後は、脅威1と同じようにsend.phpをPOSTすることで、上記でセットした問い合わせ内容が、被害者のブラウザから送信されることになります。
私のPoCでは、被害者のブラウザから「犯行予告」を問い合わせとして送るようにしましたが、他の悪用もあり得るでしょう。
NULLのトークンを空文字列にしなければ問題ないのか?
confirm.phpとsend.phpではセッション変数のトークンがNULLの場合、空文字列に置き換えています。典型的な「余計なお世話」ですが、これがなければ脆弱とはならないでしょうか。そうではありません。セッション変数のトークンがNULLなのであれば、POSTするトークン側もNULLにすれば、NULL === NULL でCSRF対策をすり抜けます。具体的には、POSTパラメータtokenを送らないことにより実現可能です。
せっかくトークンのNULLチェックをしていて、トークンがNULLということは正常な遷移ではないわけですから、この時点でエラーとして処理を終了すればCSRF脆弱性は防げました。
対策
前述のように、$_SESSION['token'] のバリデーションでセッション変数のトークンが空でないことを確認すれば脆弱性は防げますが、お勧めしたいのは、以下のステップでもトークンが空でないことを確認することです。- POSTされたトークンが$_SESSION['token']に一致しなければエラー表示
- POSTされたトークンが空であるか、または$_SESSION['token']に一致しなければエラー表示
加えて、PHP 5.6以降を使っている場合は、トークンの比較には === ではなく、hash_equalsを使うとよいでしょう。hash_equalsはタイミング攻撃を防ぐことが元々の目的ですが、引数がともにNULLの場合には FALSE を返すので、トークンがNULLなのにトークンチェックをすり抜けることが防げます。
まとめると以下のようになります。
- トークンのチェックの際には、トークンが空でないことを確認する
- 可能であれば hash_equalsによりトークンを比較する