LoginSignup
12
13

More than 5 years have passed since last update.

COOKIEによるCSRF対策メモ

Last updated at Posted at 2015-09-16

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_;
12
13
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
13