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