CSRF対策用コンポーネントとヘルパー

CSRF対策のトークンチェックは、結局自作で入れる事にして、
Token作成およびチェックのコンポーネントとヘルパーを作成しました。

プラグインにまとめても良さそうなのですが、良い名称が思い浮かばないので保留 *1


(2011/2/10 コメント指摘を受けて修正しました)



作成したコンポーネントとヘルパーのソースは末尾。

Tokenコンポーネント

Authコンポーネントの$ActionMapを見て、未設定/read以外(create, delete, update)のアクションであれば、POSTがある場合Tokenチェックを行なう。
Tokenがない、あるいは正しくない場合、処理停止。

Tokenヘルパー

value=セッションIDのハッシュ であるhiddenタグを出力
暗号化はデフォルトmd5

コントローラ・ビュー側の対応

Tokenチェックはapp_controllerのbeforeFilterで実施。

// 対CSRF:Tokenチェック
$this->Token->checkToken();


Tokenが必要なviewにはformタグ内に以下の通り追記。

echo $token->create();


create/delete/updateのいずれかだけど、Tokenチェックをさせないアクションがある場合は、個々のコントローラに以下の変数を設定。

var $disableTokenActions = array('add','mobile_add');

* で全アクションで不使用。


長時間同じセッションIDだと無用心なので、有効期限もちょっと変えました*2

-       Configure::write('Session.timeout', '3600');
+       Configure::write('Session.timeout', '432');


自前の認証コンポーネント仕様になっていますが、AuthPlusをAuthに置換すればCakePHPデフォルトのAuthコンポーネント対応で使用できるはずです。


全差分

Index: controllers/components/token.php
===================================================================
--- controllers/components/token.php    (revision 0)
+++ controllers/components/token.php    (revision 0)
@@ -0,0 +1,111 @@
+<?php
+/**
+ * CSRF対策用Tokenチェッカー
+ * Security.level = medium または low のみ
+ */
+
+class TokenComponent extends Object
+{
+
+/**
+ * Components used by TokenHelper
+ *
+ * @var array
+ * @access public
+ */
+       var $components = array('Session');
+
+       var $_modelClass;
+       var $_data = array();
+       var $_action;
+       var $_actionMap = array();
+       var $type;
+       var $useToken = false;
+       var $disableActions = array();
+
+       function initialize(&$controller)
+       {
+               $this->_modelClass = $controller->modelClass;
+               $this->_action = $controller->action;
+               if (isset($controller->params['data'])) {
+                       $this->_data = $controller->params['data'];
+               }
+
+               if (isset($controller->AuthPlus)) {
+                       $this->_actionMap = $controller->AuthPlus->actionMap;
+               } else {
+                       return ;
+               }
+
+               if (isset($this->_actionMap[$this->_action])) {
+                       $this->type = $this->_actionMap[$this->_action];
+               }
+
+               if (!isset($controller->disableTokenActions)) {
+                       $this->useToken = false;
+               } else {
+                       $this->useToken = $this->isUseToken($controller->disableTokenActions);
+               }
+
+               $this->Session->startup($controller);
+       }
+
+       /* true: Token OK */
+       function checkToken($tag_name = '__Token', $hash_type = 'md5')
+       {
+               if ($this->useToken === false) {
+                       return ;
+               }
+               $hashed_session_id = $this->get_hashed_session_id();
+
+               if ($this->_data) {
+                       if (!isset($this->_data[$this->_modelClass][$tag_name])) {
+                               $this->_blackHole();
+                       }
+                       if ($this->_data[$this->_modelClass][$tag_name] != $hashed_session_id) {
+                               $this->_blackHole();
+                       }
+               } else {
+                       return ;
+               }
+       }
+
+       /* true:Token使用 */
+       function isUseToken($disableTokenActions)
+       {
+               if ($disableTokenActions == '*') {
+                       return false;
+               }
+               if (!$this->type || $this->type == 'read') {
+                       return false;
+               }
+               if (in_array($this->_action, (array)$disableTokenActions)) {
+                       return false;
+               }
+
+               return true;
+       }
+
+       function _blackHole($msg='')
+       {
+               if (!$msg) {
+                       $msg = _('ILLEGAL POST!');
+               }
+
+               die($msg);
+       }
+
+       /* 現在のセッションIDを暗号化して取得 */
+       function get_hashed_session_id($hash_type = 'md5')
+       {
+               $session_id = $this->Session->id(null);
+
+               if (!$session_id) {
+                       $this->_blackHole('No Session.');
+               }
+
+               return Security::hash($session_id. Configure::read('Security.salt'), $hash_type);
+       }
+
+}
+
Index: views/helpers/token.php
===================================================================
--- views/helpers/token.php     (revision 0)
+++ views/helpers/token.php     (revision 0)
@@ -0,0 +1,38 @@
+<?php
+/**
+ * CSRF対策用Token出力ヘルパー
+ * 要Formヘルパー
+ */
+
+class TokenHelper extends AppHelper {
+/**
+ * Other helpers used by TokenHelper
+ *
+ * @var array
+ * @access public
+ */
+       var $helpers = array('Form', 'Session');
+
+       /* Tokenをセットしたhiddenタグ出力 */
+       function create($tag_name = '__Token', $hash_type = 'md5')
+       {
+               $hashed_id = $this-> get_hashed_session_id($hash_type);
+
+               return $this->Form->input($tag_name, array(
+                       'type' => 'hidden',
+                       'value' => $hashed_id,
+                       )
+               );
+       }
+
+       /* 現在のセッションIDを暗号化して取得 */
+       function get_hashed_session_id($hash_type = 'md5')
+       {
+               $session_id = $this->Session->id();
+
+               return Security::hash($session_id. Configure::read('Security.salt'), $hash_type);
+       }
+
+}
+
+
Index: controllers/app_controller.php
===================================================================
--- controllers/app_controller.php      (revision 196)
+++ controllers/app_controller.php      (working copy)
@@ -21,17 +21,23 @@
        var $isAdmin = false;
        var $isMobile = false;

       var $components = array(
               'AuthPlus',
               'Acl',
+               'Token'
       );

        /* ACL */
        // 追加アクション用 crudMap
        var $actionMapPlus = array();

+       // POSTのTokenチェックをしないアクション
+       var $disableTokenActions = array();
+
        function beforeFilter()
        {
                parent::beforeFilter();

@@ -46,6 +52,9 @@
                        $this->AuthPlus->actionPath = 'controllers/';
                        $this->AuthPlus->authorize = 'crud';

+                       // 対CSRF:Tokenチェック
+                       $this->Token->checkToken();
+
                        // 認証アクション設定
                        if (Configure::read('mobileUserAgent')) {
                                $this->AuthPlus->loginAction = '/m/users/login';


Index: controllers/users_controller.php
===================================================================
--- controllers/users_controller.php    (revision 196)
+++ controllers/users_controller.php    (working copy)
@@ -2,7 +2,11 @@
 class UsersController extends ModuleController {

        var $name = 'Users';
        var $helpers = array(
               'Html',
               'Form',
+               'Token'
        );

        /* ACL */
        // 追加アクション用 crudMap
@@ -11,6 +15,8 @@
                'change_password' => 'update',
        );

+       var $disableTokenActions = array('add','mobile_add');
+
        function beforeFilter() {

                parent::beforeFilter();
--- views/users/edit.ctp        (revision 197)
+++ views/users/edit.ctp        (working copy)
@@ -7,6 +7,7 @@
                                'label' => __('YourName', true),
                        )
                );
+               echo $token->create();
        ?>
        </fieldset>
 <?php echo $form->end('Submit');?>
--- config/core.php.sample      (revision 196)
+++ config/core.php.sample      (working copy)
@@ -122,7 +122,7 @@
  * Session time out time (in seconds).
  * Actual value depends on 'Security.level' setting.
  */
-       Configure::write('Session.timeout', '3600');
+       Configure::write('Session.timeout', '432'); // 60 * 60 * 12 / 100
 /**
  * If set to false, sessions are not automatically started.
  */

*1:気持ちとしてはTokenプラグインなのですが、呼び出しがToken.Tokenになるのがどうにも ^^;

*2:えいや、の値。半日がかりでブラウザ上で投稿書くと無効になります。 ・・・そんな力の入った長文は、テキストエディタで随時保存しつつ書いてコピペしてください、と私は言いたい。