COOKIEによるCSRF対策メモ

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

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

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

```php: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_;
```