COOKIEによるCSRF対策メモ

  • 7
    いいね
  • 4
    コメント
この記事は最終更新日から1年以上が経過しています。

FuelPHPのソースコード(Security.php)を眺めていて気付いたこと。
CSRF対策で所謂PHPのセッション機構そのものは利用しておらず、サーバ側でtokenを保持するということはしていない。
基本、送信されたCOOKIE値とパラメータ値の比較をもってCSRF対策としている。
これは「COOKIEは外部ドメインサイトからの読み書きが不可である」という前提に立っている。

下記ソースコードは「COOKIE値とPOST値の比較」でCSRF対策のチェックとするサンプル。
フォームとajaxでの利用を想定としている。

なお、任意の値を送信COOKIEとPOST値に設定すれば、Csrf::check自体はtrueになるので、実際にはセッションによる認証機構を前提とする。
(そもそもセッションによる認証を必要としなければ、CSRF対策自体不要)

csrf.php
<?php
class Csrf {

    static $key  = '_csrf';
    static $token = '';

    /**
     * tokenを作成し、cookieに設定
     */
    public static function create() {

        self::$token = self::gen_token();


        setcookie(self::$key, self::$token);
    }

    /**
     * token生成
     */
    public static function gen_token() {

        $token = bin2hex(openssl_random_pseudo_bytes(16));

//      $token = sprintf('%04x%04x%04x%04x%04x%04x%04x%04x',
//          mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
//          mt_rand( 0, 0xffff ),
//          mt_rand( 0, 0x0fff ) | 0x4000,
//          mt_rand( 0, 0x3fff ) | 0x8000,
//          mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )
//       );


// FuelPHPのSecurity::generate_tokenより。salt を利用する。
//      $token_base = time() . uniqid() . \Config::get('security.token_salt', '') . mt_rand(0, mt_getrandmax());
//      if (function_exists('hash_algos') and in_array('sha512', hash_algos()))
//      {
//          $token = hash('sha512', $token_base);
//      }
//      else
//      {
//          $token = md5($token_base);
//      }

//      PHP7
//      $token = bin2hex(random_bytes(16));

        return $token;
    }

    /**
     * token取得
     */
    public static function fetch() {
        return self::$token;
    }

    /**
     * token と cookie値を比較
     */
    public static function check() {
        $val = !empty($_COOKIE[self::$key]) ? $_COOKIE[self::$key] : '';
        if (empty($val)) {
            return false;
        }

        self::create();

        $postval = !empty($_POST[self::$key]) ? $_POST[self::$key] : '';
        if ($val === $postval) {
            return true;
        }

        return false;
    }
}

// 登録処理などはこのへんで行う。実際には認証情報取得が前提。
if (!empty($_POST['btn_reg'])) {
    if (Csrf::check()) {
        echo 'send ok.';
    } else {
        echo 'send ng.';
    }
    exit();
}

Csrf::create();

$fetch = Csrf::fetch();

// viewやtemplateを利用する場合、このあたりからテンプレート側の処理になる

echo <<<_EOD_
<html>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>

<h1>フォーム</h1>
<form method="POST">
<input type="hidden" name="_csrf" value="{$fetch}" />
<input type="submit" name="btn_reg" value="PUSH" />
</form>

<h1>ajax</h1>
<a href="#" id="push">ajax</a>

<script>
// 実際には外部JavaScriptに記述するような部分
$(function(){
    // cookie読み込み。fuelphpの Security::js_fetch_token 辺りを参考に。
    var csrf_token = function() {

        if (document.cookie.length > 0)
        {
            var c_name = '_csrf',
            c_start = document.cookie.indexOf(c_name + "="),
                c_end;
            if (c_start != -1)
            {
                c_start = c_start + c_name.length + 1;
                c_end = document.cookie.indexOf(";" , c_start);
                if (c_end == -1)
                {
                    c_end=document.cookie.length;
                }
                return unescape(document.cookie.substring(c_start, c_end));
            }
        }
        return "";
    }

    // cookieからtoken取得
    $('#push').on('click',function(e){
        $.ajax({
            url : 'csrf.php',
            type: 'POST',
            data: { _csrf : csrf_token(), btn_reg : 'PUSH', },
        }).then(
            function(msg){ alert(msg); }
        );
        return false;
    });
});
</script>

</html>
_EOD_;