-
CSRF対策
CSRF(クロスサイトリクエストフォージェリ) 対策についてはwikiを参照ください。
つまりは、<form>
から何らかの値を送信して、サーバーに処理させる場合、この対策を怠らないようにしましょうということ。
基本的には、form の内部に、<input type="hidden">
で値を送信し、SESSION
に保存された値と一致するかどうかを確認する。
このCSRF対策には、二重送信を防止するという副産物もあり、正規ユーザーに対しては、「CSRFを検知したので、処理を中断しました」というような「不正」を表現するのではなく、「二重送信されたので処理を中断しました。」のようなメッセージにすると良いかもしれません。
##実装
classes/common/Csrf.class.php
にCsrf
クラスを定義しました。
###classes/common/Csrf.class.php
<?php
namespace MyApp\common;
/**
* Csrf.class.php
*/
class Csrf
{
/**
* トークン
* @var string
*/
private static $token = null;
/**
* 初期化
*/
private static function init()
{
self::$token = sha1(uniqid());
}
/**
* CSRF用にトークンを生成する
* @return string
*/
public static function get()
{
if (is_null(self::$token)) {
self::init();
}
$_SESSION['csrf_token'] = self::$token;
return self::$token;
}
/**
* CSRFをチェックする
* @return boolean
* @throws ApplicationErrorException
*/
public static function check()
{
$csrf_token = (isset($_SESSION['csrf_token'])) ? $_SESSION['csrf_token'] : null;
$_SESSION['csrf_token'] = null;
if (filter_input(INPUT_POST, 'csrf_token') !== $csrf_token) {
// 二重送信されたので処理を中断しました。
throw new InvalidErrorException(ExceptionCode::INVALID_CSRF_ERR);
}
return true;
}
}
###classes/common/ExceptionCode.class.php
<?php
namespace MyApp\common;
/**
* ExceptionCode.class.php
* @since 2015/07/25
*/
class ExceptionCode
{
/**
* InvalidError
*/
const INVALID_ERR = 1000;
const INVALID_LOCK = 1001;
const INVALID_LOGIN_FAIL = 1002;
const ARGUMENT_REQUIRED = 1003;
const INVALID_CSRF_ERR = 1004;
/**
* ApplicationError
*/
const APPLICATION_ERR = 2000;
/**
* SystemError
*/
const SYSTEM_ERR = 3000;
const TEMPLATE_ERR = 3001;
const TEMPLATE_ARG_ERR = 3002;
/**
* エラーメッセージ
* @var array<string>
*/
private static $_arrMessage = array(
self::INVALID_ERR => 'エラーが発生しました。'
, self::INVALID_LOCK => 'アカウントがロックされています。'
, self::INVALID_LOGIN_FAIL => 'ログインに失敗しました。'
, self::INVALID_CSRF_ERR => '二重送信されたので処理を中断しました。'
, self::ARGUMENT_REQUIRED => 'メールアドレスおよびパスワードは入力必須です。'
, self::APPLICATION_ERR => 'アプリケーション・エラーが発生しました。'
, self::SYSTEM_ERR => 'システム・エラーが発生しました。'
, self::TEMPLATE_ERR => 'テンプレート[%s]が見つかりません。'
, self::TEMPLATE_ARG_ERR => '引数に[%s]は利用できません。'
);
/**
* エラーメッセージを取得する
* @param int $intCode
* @return string
*/
static public function getMessage($intCode)
{
if (array_key_exists($intCode, self::$_arrMessage)) {
return self::$_arrMessage[$intCode];
}
}
}
##利用方法
前述のように、form の中に hidden で梅子見るのですから、以下のような使い方になります。
<form method="post">
// その他のフォーム部品
<input type="hidden" name="csrf_token" value="<?= Csrf::get(); ?>">
</form>
基本的に form
を利用するところには、CSRF対策を施す必要がありますが、開発メンバーの全員が CSRF
について理解した上で、利用しているとは限りません。「処理に関係ない値」と思ってわざわざ削除してしまわないとも限りません。多くのフレームワークで、たかだか <form>
を生成するためのヘルパーメソッドが用意されているには、理由があり、この CSRF対策
が formヘルパー
で自動的に付与される仕組みになっています。
テンプレート・エンジン Smarty
には、カスタムプラグイン機能が用意されており、form
のように、開始タグと終了タグを生成するためのブロック関数を拡張することができます。
###smarty/plugins/block.form.php
<?php
/**
* block.form.php
*/
function smarty_block_form($params, $contents)
{
if (!$contents) {
return;
}
$attr = [];
foreach ($params as $key => $val) {
$attr[] = sprintf('%s="%s"', $key, $val);
}
$attribute = (count($attr) > 0) ? ' ' . implode(' ', $attr) : '';
$html = sprintf('<form%s>', $attribute);
$html .= $contents;
$html .= sprintf('<input type="hidden" name="csrf_token" value="%s">'
, MyApp\common\Csrf::get()
);
$html .= '</form>';
return $html;
}
テンプレート側については以下のようにして呼び出します。
{form method="post"}
と {/form}
の部分がそれにあたります。
###smarty/templates/unlock.tpl
{block name='meta'}
<style type="text/css">
.login-box, .register-box {
margin: 20px auto;
}
</style>
{/block}
{block name='content'}
<div class="login-box">
<div class="login-logo">
<a href="/">
<b>Admin</b>
LTE
</a>
</div>
<!-- /.login-logo -->
<div class="login-box-body">
{if $success|default:null}
<p class="login-box-msg">アカウントロックを解除しました。</p>
<p class="login-box-msg">
<a href="/">ログインページ</a> よりログインしてください。
</p>
{else}
{if $is_lock}
<p class="login-box-msg">ロックを解除するには、下のボタンを押してください。</p>
{form method="post"}
<div class="row">
<div class="col-xs-6 col-xs-offset-3">
<button type="submit" class="btn btn-primary btn-block btn-flat">
ロックを解除する
</button>
</div>
</div>
{/form}
{else}
<p class="login-box-msg">アカウントはロックされていません。</p>
<p class="login-box-msg">
<a href="/">ログインページ</a> よりログインしてください。
</p>
{/if}
{/if}
</div>
<!-- /.login-box-body -->
</div>
<!-- /.login-box -->
{/block}
{block name='script'}
{/block}
同様に index.tpl
についても行っておけば良いでしょう。
###Csrf::check()の使い方
/**
* ロックを解除する
* @return boolean
*/
public static function unlock()
{
if (null == filter_input_array(INPUT_POST)) {
return;
}
// CSRFチェック
Csrf::check();
$token = filter_input(INPUT_GET, 'token');
Db::transaction();
$objUserModel = new UserModel();
$objUserModel->getModelByToken($token);
$objUserModel->setLoginFailureCount(0)
->setLoginFailureDatetime(NULL)
->setToken('')
->save();
DB::commit();
return true;
}
##GitHub レポジトリ
これまでの解説で作成したソースコードについては、こちらに。