Hatena::ブログ(Diary)

モトクロスとプログラムと粉砕骨折と RSSフィード Twitter

2009-12-19

[][]CakePHPのSecurityコンポーネントの機能を縮小してみた

プログラミングのお時間です。

いまCakePHP使って色々と遊んでるんだけど、Amebaナウで話題になったCSRF対策に、Serucityコンポーネントを使ってみた。


ただこのコンポーネント、高機能すぎてちょっと使いづらい。

自分としてはToken使ったページ遷移チェックだけで良かったんだけど、ご丁寧にフォーム改ざんチェックとかもしてくれてる。もちろん普通に使う分には問題無いです。


だけどJavaScriptで動的にフォームを変更してたり、ちょっと変わった画面遷移してたりすると、途端にチェックに不合格になってしまう。

最初CakePHPの使い方がよく分からなくて、妙な画面遷移とアクションの組み合わせにしてしまったのは僕です。


だから軽量版のSecurityコンポーネントを作ってみた。

いや。作ってみたというと偉そうだ。正確にはSecurityComponentの機能をごっそり消して、Tokenチェックだけするように改変してみた。正直なところこれでイイかよく分からん感じだけど、今の所自分の要件は満たしてる。(気がする)


ソースを晒しておくので必要なら適当に使って下さい。app/controller/componentsの下に、easy_security.phpって名前で保存して使ってね。ベースにしたCakePHPのバージョンは1.2.5です。


<?php
/**
 * PHP version 5
 *
 * CakePHP(tm) : Rapid Development Framework (http://www.cakephp.org)
 * Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @filesource
 * @copyright     Copyright 2005-2008, Cake Software Foundation, Inc. (http://www.cakefoundation.org)
 * @link          http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
 * @modifiedby    Shingo Mori (http://connectionworks.jp)
 * @lastmodified  2009/12/19-23:20
 * @license       http://www.opensource.org/licenses/mit-license.php The MIT License
 *
 * CakePHP純正SecurityComponentの機能縮小バージョン。
 * フォームの改ざんチェックとかHTTP認証とかを無くして、指定したアクションでだけTokenによる
 * ページ遷移チェックを行うようにした。
 */
class EasySecurityComponent extends Object {
    
    /**
     * チェックNG時に呼ばれるコントローラー上の関数を指定
     * @var string 関数名
     */
    public $blackHoleCallback = null;
    
    /**
     * チェック対象のアクション配列
     * @var array
     */
    private $requireAuth = array();
    
    /**
     * 利用するComponents定義
     * @var array
     */
    public $components = array('RequestHandler', 'Session');
    
    /**
     * 呼び出しアクション
     * @var string
     */
    private $_action = null;

    /**
     * 開始処理
     * @param app_controller $controller 呼び出し元Controllerオブジェクト
     */
    public function startup(&$controller) {
        $this->_action = strtolower($controller->action);
        if (in_array('*', $this->requireAuth) || in_array($this->_action, $this->requireAuth)) {
            if ($this->RequestHandler->isPost() || $this->RequestHandler->isPut()) {
                if ($this->_validatePost($controller) === false) {
                    if (!$this->blackHole($controller, 'auth')) {
                        return null;
                    }
                }
            }
            $this->_generateToken($controller);
        }
    }

    /**
     * ページ遷移チェック対象アクションを設定
     * @return string(,string,...) チェック対象アクションを指定。カンマ区切りで複数指定可
     *                             nullとか'*'を指定すると全アクションでチェックする
     */
    public function requireAuth() {
        $actions = func_get_args();
        $this->requireAuth = (empty($actions)) ? array('*'): $actions;
    }

    /**
     * チェックNG時処理
     * @param app_controller $controller 呼び出し元Controllerオブジェクト
     * @param string $error エラー文字列
     */
    public function blackHole(&$controller, $error = '') {
        $this->Session->del('_Token');

        if ($this->blackHoleCallback == null) {
            $controller->redirect(null, 404, true);
        } else {
            return $this->_callback($controller, $this->blackHoleCallback, array($error));
        }
    }

    /**
     * ページ遷移チェック
     * @param app_controller $controller 呼び出し元Controllerオブジェクト
     * @return boolean 結果
     */
    private function _validatePost(&$controller) {
        if (empty($controller->data)) {
            return true;
        }
        $data = $controller->data;

        if (!isset($data['_Token']['key'])) {
            return false;
        }
        $token = $data['_Token']['key'];

        if ($this->Session->check('_Token')) {
            $tokenData = unserialize($this->Session->read('_Token'));
            if ($tokenData['expires'] < time() || $tokenData['key'] !== $token) {
                return false;
            }
        } else {
            return false;
        }
        
        return true;
    }
    
    /**
     * ページ遷移用Tokenの発行
     * @param app_controller $controller 呼び出し元Controllerオブジェクト
     * @return boolean 結果
     */
    private function _generateToken(&$controller) {
        $authKey = Security::generateAuthKey();
        $expires = strtotime('+' . Security::inactiveMins() . ' minutes');
        $token = array(
            'key' => $authKey,
            'expires' => $expires,
        );

        if (!isset($controller->data)) {
            $controller->data = array();
        }

        if ($this->Session->check('_Token')) {
            $tokenData = unserialize($this->Session->read('_Token'));
            $valid = (
                isset($tokenData['expires']) &&
                $tokenData['expires'] > time() &&
                isset($tokenData['key'])
            );

            if ($valid) {
                $token['key'] = $tokenData['key'];
            }
        }
        $controller->params['_Token'] = $token;
        $this->Session->write('_Token', serialize($token));

        return true;
    }
    
    /**
     * コールバック関数呼び出し
     * @param app_controller $controller 呼び出し元Controllerオブジェクト
     * @param string $method コントローラー上のメソッド名
     * @param array $params パラメータ
     */
    private function _callback(&$controller, $method, $params = array()) {
        if (is_callable(array($controller, $method))) {
            return call_user_func_array(array(&$controller, $method), empty($params) ? null : $params);
        } else {
            return null;
        }
    }
}

使い方はCakePHPのSecurityコンポーネントと一緒で、こんな感じにControllerに組み込む。

var $components = array('EasySecurity');

public function beforeFilter()
{
    // ここにページ遷移チェックをしたいアクションを書く
    $this->EasySecurity->requireAuth('save', 'delete');
}

JavaScriptで数フォーム増減させる程度だったら、SecurityコンポーネントのdisabledFieldsに無視するフィールド名追加した方がいいけどね!


変更:2009/12/27

requireAuth()で'*'を指定された場合の処理をすっかり忘れておったので、ちょっと添付ソースを修正した。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/sngmr/20091219/1261235966
Connection: close