PHPでCSRF対策を自動的に行う方法

(更新日: 2018/06/05)

PHPでWebページにCSRF対策を追加するのは簡単です。全てのページにCSRF対策を追加する場合、ファイルを1つインクルードする以外、ほとんど何も行う必要がありません。

CSRF Protection for PHPの機能

  • 自動的にHTMLフォームやリンクにCSRFトークンを追加
  • CSRFトークンの有効期限を設定可能

まだまだ多くのアプリケーションが固定かつ有効期限を設定しないのCSRFトークンを使っています。この状態では一旦CSRFトークンを盗まれると、攻撃者にいつまででも攻撃される可能性があります。

このスクリプトの場合、有効期限付きのCSRFトークンをWebページに自動追加して防御します。

後で記載するようにPHP 7.1でoutput_rewrite_var()のバグを完全に直しました。なのでPHP 7.1以降の利用がお勧めです。

リポジトリと使い方

サンプルコードはgithubに公開しています。ライセンスはMITです。

https://github.com/yohgaki/php-csrf-protection

使い方は簡単で、srcディレクトリにあるcsrf_init.phpをインクルードし、Exceptionが発生した場合に対応するだけです。

これでPHPから出力する全てのWebページがCSRF攻撃から防御できます。

出力バッファを使っているので、出力が始まる前に上記のコードを実行する必要があります。

PHP 7.1でoutput_rewrite_var()のバグを完全に直しました。この際、セッションモジュールと共有していた出力バッファを分離し、output_*()関数が独立した出力バッファを持つようにしています。更に、PHP 7.1より古いPHPではfrom以外にもa、iframeなど色々書き換えていたのをformのみに変更しています。

PHP 7.1未満でTranSIDを有効にすると問題になります。TranSIDを有効にしている方は少ないと思いますが、一応PHP 7.1以降での利用を推奨します。

※ このサンプルはhash_hkdf()を利用しているのでPHP 7.1以上が必要です。

サンプルコードの実行

実際に動作するサンプルコードは次の手順で実行できます。

この後、ブラウザで

http://127.0.0.1:8888/csrf_test.php

にアクセスします。最初のアクセスにはCSRFトークンが設定されていないので、必ずエラーになります。「Click here to return page」のリンクをクリックするとページが表示されます。

このページにはform内に隠しinput、href属性に変数が追加されていることを確認できます。

TEST LINKをクリックするとクエリ文字列にCSRFトークンが追加されています。これを変更すると例外が発生し、無効なリクエストになることも確認できます。

オンラインで試す場合、これが利用できます。

https://sample.ohgaki.net/php-csrf-protection/example/

コードの紹介

心臓部となるコードはとても簡単です。次の2つがCSRFトークンを生成、検証する関数です。

HKDFを使い、シードとなる$secret、設定された有効期限から強力なCSRFトークンを導出します。

参考:

https://github.com/yohgaki/php-csrf-protection/blob/master/src/csrf_protection.php

 

上記の関数を個別に利用することもできますが、csrf_init.phpを使うと自動的にCSRF対策できます。

https://github.com/yohgaki/php-csrf-protection/blob/master/src/csrf_init.php

 

自動的にCSRFトークンを追加しているのは output_add_rewrite_var() です。

恐らく多くのPHPユーザーは output_add_rewrite_var() を使ったことが無いと思います。この関数は出力したWebページデータを解析し、指定された変数を指定されたHTMLの要素に追加します。

HTML要素の指定はurl_rewriter.tagsで設定します。

tag_name=attribute_name

をカンマ区切りで指定します。

ini_set(‘url_rewriter.tags’, ‘form=,a=href’);

この定義は、formタグに隠しinputタグを追加し、aタグのherf属性のURLにパラメーターを追加します。例えば、form=actionとするとactionに指定されたURLにパラメーターを追加します。img=srcとするとimgタグのsrc属性のURLにパラメーターを追加します。

formタグの取り扱いだけが特別で隠しinputタグを追加します。他は指定したタグのURL属性にパラメーターを追加します。URL属性以外も指定できますが、img=idとし、id属性の値が”banner”だとすると”banner?param_name=param_value”というあまり意味のない結果になります。

出力バッファのバグも色々直してきたので、この関数の存在と使い方は追加された時から知っていました。しかし、PHP 7.1まで使うことをあまりお勧めできない関数でした。PHP 7.1で構造上の問題を直してやっと、使いましょう、と言える状態なりました。いつかこれを書こうと思っていたら2年以上経過してしまいました。

まとめ

自動的かつ有効期限付きの強力なCSRF対策をすることはPHPではとても簡単です。php.iniのauto_prepend_fileを使えば、コードの変更も必要ありません。内部用に作ってあるアプリケーションなどでCSRF対策をしていない物でも、コード変更なしで完璧にCSRF対策できます。

CSRF対策があるアプリケーションでも、漏れが心配な場合でコード検査をするのが面倒なときにも便利です。HTML FormでないGETリクエストも含め、全てCSRF対策できます。/admin のパス以下だけ追加のCSRF対策を入れる、といった使い方も可能です。1

ただし、HTTP Refererによりクエリ文字列からトークンが漏洩する場合があります。これはReferer-Policyとそれをサポートするブラウザを利用するか、HTTPSを利用してREFERERが送られないようにする必要があります。

色々なところで試してください。

参考:


  1. 内部ネットワークからインターネットを使う場合、そもそも外部のネットワークのWebサイトなどを利用しても内部ネットワークに対してCSRFやXSS攻撃が行えないような仕組みを利用すべきです。参考1参考2参考3 

Comments

comments

Pocket