PHP

COOKIEによるCSRF対策メモ

More than 3 years have passed since last update.

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_;