CodeIgniterの学習 23 - ワンタイムチケット(ワンタイムトークン)の機能を作って、CSRF対策をちょっとした気分になってみる
今日はワンタイムチケット(トークン)の機能を実装してみる。
CodeIgniter Version.1.6.3 には標準では、ライブラリ・ヘルパ共に該当のものは存在しないようだ。
POSTするフォーム毎に、セッション + フォーム側のhidden項目を使って、
バリデーションチェックすれば別にそれでもいいのだけれど、
毎回書くのが面倒なので、誰か作っていないか探してみた。
探してみると、http://codeigniter.com/forums/viewthread/71312/ にヘルパとして実装した投稿が有ったのだが、
$_SESSIONを使用する方式だったので、これをdb_session専用にライブラリ化 + ヘルパ方式に改造することにした。
使う人はいないかもしれんが、改造したモノを貼っておく。一応動くけど当然無保証。
db_sessionを使っていない時は動かないけど、知らん。(追記:CI1.7以降だと、db_session → sessionに全置換&DB使用でも動く。)
元ネタは、http://codeigniter.com/forums/viewthread/71312/ ですが、改造版は殆ど跡形も有りません。
動作画面
こんな感じ、
<?=form_csrf('tasklist_edit');?>
をビュー側に貼り付けるとワンタイムトークンが自動生成される。
hidden項目のnameの後ろ6文字と、valueは毎回ランダムに変わる。
(ここでは動きが分かるように、hidden項目を画面に出している。)
セッション側にもこの値が自動的に保存される。
submit後に戻るボタンで画面を戻り再送信したり、外部から直接ポストされたりして
ワンタイムトークンが異なったり、トークンが存在しない時は、エラーとして弾く。
この時には、nameとvalueの異なるトークンが再生成される。
(よりランダムにするためにtkn_のprefixが不要ならはずしてもいい。名前がぶつかるのが嫌だから付けているだけ)
元ネタからの変更点
- Db_session専用に改造($_SESSION は使わない)
- ヘルパをやめてライブラリ(いんちきクラス)化した。
- トークンのバリデーションは自動で走るのではなく、明示的に呼び出すようにした。
- submit先が、元の画面と異なる場合でも動くようにした。(以下方式A)
- submit先が同一の時にurl_suffixと、末尾の/(の連続)を強制的に取り除いた値で判定するように改造した。
デフォルトだと 真偽チェック時のセッション側キーが、$_SERVER["REQUEST_URI"] に依存しているので、
formのsubmit先が別のurlだとうまく動きません。
この時は、フォームヘルパでform_csrf('複数画面にまたがる任意の同一なsuffix' );
と引数を指定することで、submit先が異なっても対応可能にしているつもり。
特徴と簡単な使い方
1)ビュー側(特徴:)
新たに追加したフォームヘルパのform_csrfをフォーム内で使うと、
フォームに毎回アクセスする度に自動的に、
ランダムなnameと、ランダムなvalueを持つhidden項目(ワンタイムチケット)を生成してくれる。
同時に、セッション(db_session)内にも照合用のデータを保持してくれる。
(使い方:)
方式A:編集画面とPOST先の遷移URLが違う時
//編集画面が、/tasklist/edit POST先が /tasklist/submit の時の例 <?=form_open('/tasklist/submit/'); ?> <?=form_csrf('tasklist_edit');?>
のように、form_csrfを、任意の引数付きで書く。
方式B:編集画面とPOST先の遷移URLが同一の時
//編集画面が、/tasklist/edit POST先も /tasklist/edit の時の例 <?=form_open('/tasklist/edit/'); ?> <?=form_csrf();?>
のように、form_csrfを、引数なしで書く。
こうすることで、view側に
<input type="hidden" name="tkn_uqxfn7" value="0206d72bf4722601cf67f07b44986c907ad270b7" />
みたいな、nameとvalueがランダムなhidden項目が生成される。
同時に、db_session内に、
"tkn_varname_tasklist_edit"=>"tkn_uqxfn7"
"tkn_value_tasklist_edit"=>"0206d72bf4722601cf67f07b44986c907ad270b7"
といった照合用のデータが保存される。
2)コントローラ側
(特徴:)
トークンは、
$this -> load -> library( 'csrf' );
でライブラリをロードし、
$this -> csrf -> check("任意の引数");
又は
$this -> csrf -> check();
でチェックするだけ。
エラーメッセージは、
$this -> csrf -> get_message( "tasklist_edit" );
みたいに取得できる。
(使い方:)
方式A:編集画面とPOST先の遷移URLが違う時
$this -> csrf -> check( "tasklist_edit" )
のように、
check("任意の引数")で 任意の引数のトークンの整合性をチェックして、真偽が戻る。
方式B:編集画面とPOST先の遷移URLが同一の時
$this -> csrf -> check()
のように、
check(引数無し)で、REQUEST_URIのトークンの整合性をチェックして、真偽が戻る。
新設するファイルとソース
ヘルパとライブラリの計2つ。
元ネタを改造してだいぶ変わってしまった。
勢いで適当に改造したのでまだ変えるかも。
(09/03/28追記:改良・拡張版ソース→ http://d.hatena.ne.jp/dix3/20081111/1226350226 )
あとこのソースには、ワンタイムトークン自身のライフタイム(有効時間)のチェックが入ってなくて、
セッションの有効時間のみに依存しているので、この部分を後で何とかしたい。
(db2_session改造版のdb_sessionの場合は、画面アクセスする度にセッションidが切り替わるので有効時間はきわめて短いのだが。)
(複数トークンの保持といった使い方も出来るはずだがこちらは未テスト。コメントはまだ整備していない。)
1)ヘルパ:application/helpers/MY_form_helper.php
form_helper.phpを継承し、MY_form_helper.phpを作る。(既存のヘルパ関数も呼べる)
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); if ( ! function_exists( 'form_csrf' ) ) { /** * * @method string form_csrf($suffix, $varlen, $str_to_shuffer) returns a constructed hidden input field of the csrf token * @param int $varlen the length of the input field name that will be generated * @param string $str_to_shuffer the string that will be used to generate the input field name * @return string the hidden input field */ function form_csrf( $suffix = "" , $varlen = 6, $str_to_shuffer = "abcdefghijklmnopqrstuvwxyz0123456789_" ) { $CI = &get_instance(); if ( !isset( $CI -> csrf ) || !is_object( $CI -> csrf ) ) { // csrfがロードされていないときのみ読み込む $CI -> load -> library( 'csrf' ); } $CI -> csrf -> init( $suffix ); $CI -> csrf -> set_token( $varlen, $str_to_shuffer ); $dbg = false;//デバグ用 if ( $dbg ) { return "name:" . $CI -> csrf -> get_varname() . ",value:" . form_input( $CI -> csrf -> get_varname(), $CI -> csrf -> get_value() ); } else { return form_hidden( $CI -> csrf -> get_varname(), $CI -> csrf -> get_value() ); } } } ?>
2)ライブラリ:application/libraries/Csrf.php
元ネタをdb_session向けに適当に改造。原型をとどめていない。
db_sessionって、$_SESSION["hoge"]["fuga"]=$a みたいに2次元以上のキーが持てないので、
こんな事になってしまっている。
散らばるのが嫌ならarrayで突っ込むか、serializeして詰め込む方がいいかも。
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); // ワンタイムチケット(ワンタイムトークン)のライブラリ俺俺改造 // 元ネタと使い方 http://codeigniter.com/forums/viewthread/71312/ // inspired http://codeigniter.com/forums/viewthread/71312/ // changes 変更点 // 1.Db_session専用に改造($_SESSION は使わない) // 2.ヘルパをやめてライブラリ(いんちきクラス)化した。 // 3.トークンのバリデーションは自動で走るのではなく、明示的に呼び出すようにした。 // 4.submit先が、元の画面と異なる場合でも動くようにした。(以下方式A) // 5.submit先が同一の時にurl_suffixと、末尾の/(の連続)を強制的に取り除いた値で判定するように改造した。 // デフォルトだと 真偽チェック時のセッション側キーが、$_SERVER["REQUEST_URI"] に依存しているので、formのsubmit先が別のurlだとうまく動きません。 // この時は、フォームヘルパでform_csrf("複数画面にまたがる任意の同一なsuffix");と引数を指定することで、submit先が異なっても対応可能にしているつもり // 改造内容の説明 http://d.hatena.ne.jp/dix3/20081017/1224196292 class Csrf { var $CI ; var $token_suffix = ""; //デフォルト$_SERVER["REQUEST_URI"]から url_suffixと、末尾の/(の連続)を強制的に取り除いた値 var $token_prefix = "tkn_"; //トークンのprefix、毎回ランダムで生成しているので、この時に他のフォームの名前と偶然にぶつからないようにするため var $key_varname_base = "varname_"; var $key_varname=""; var $key_value_base = "value_"; var $key_value=""; var $key_message=""; //var $error_message = "A form re-post or an unknown error has occurred."; var $error_message = "フォームが再送信されたか、送信時に不明なエラーが発生しました。再度送信してください。"; // コンストラクタ function Csrf() { $this -> CI = &get_instance(); if ( !isset($this -> CI -> db_session) || !is_object( $this -> CI -> db_session ) ) { // db_sessionが開始していないときのみ読み込む $this -> CI -> load -> library( 'db_session' ); } $this -> CI -> load -> helper( 'security' ); //dohashを使えるようにするためだけ } // ワンタイムトークンの初期化 function init( $suffix = "" ) { $this -> _set_suffix ( $suffix ); } // ワンタイムトークンのチェック、セッション内の値を即時破棄したくないときは $clean = falseで呼ぶ function check( $suffix = "", $clean = true ) { $result = false; //デフォルトはfalse $this -> _set_suffix ( $suffix ); if ( isset( $_POST ) && count( $_POST ) > 0 ) { if ( $this -> CI -> db_session -> userdata( $this -> key_varname ) ) { if ( $this -> _is_token_valid() ) { $result = true; //ワンタイムトークン正常時のみtrueで返す } } } else { // POSTが無ければtrueで戻しておく、破棄もしない $result = true; } if(!$result){ $this->set_message( $this->error_message , $suffix ); } if ( $clean ) { // セッション内の値を破棄 $this -> clean($suffix , $result); } return $result; } // ワンタイムトークンの破棄 /** * * @method void clean() clears the session variables that store the csrf token */ function clean( $suffix = "" ,$result = false) { $this -> _set_suffix ( $suffix ); $this -> CI -> db_session -> unset_userdata( $this -> key_varname ); $this -> CI -> db_session -> unset_userdata( $this -> key_value ); } // ワンタイムトークンのエラーメッセージセット function set_message($msg="" , $suffix="") { $this -> _set_suffix ( $suffix ); if($suffix){ //$suffixが指定されていて遷移先が異なる場合は、flashdataを使ってみる $this -> CI -> db_session -> set_flashdata($this->key_message,$msg); }else{ $this -> CI -> db_session -> set_userdata($this->key_message,$msg); } } // ワンタイムトークンのエラーメッセージ取得 function get_message($suffix="") { $this -> _set_suffix ( $suffix ); if($suffix){ //$suffixが指定されていて遷移先が異なる場合は、flashdataを使ってみる $msg = $this -> CI -> db_session -> flashdata($this->key_message) ; }else{ $msg = $this -> CI -> db_session -> userdata($this->key_message) ; //一度取得したエラーメッセージは消去する $this -> CI -> db_session -> unset_userdata( $this->key_message ); } return $msg; } // ワンタイムトークンの生成 /** * * @method void set_token($varlen, $str_to_shuffer) construct a random input field name and assign the token to it. * @param int $varlen the length of the input field name that will be generated * @param string $str_to_shuffer the string that will be used to generate the input field name */ function set_token( $varlen = 3, $str_to_shuffer = "abcdefghijklmnopqrstuvwxyz0123456789_" ) { $start_pos = mt_rand( 0, ( strlen( $str_to_shuffer ) - $varlen ) ); $this -> CI -> db_session -> set_userdata( $this -> key_varname , $this->token_prefix . substr( str_shuffle( $str_to_shuffer ), $start_pos, $varlen ) ); $this -> CI -> db_session -> set_userdata( $this -> key_value, dohash( microtime() . mt_rand() ) ); } /** * * @method string get_varname($varlen, $str_to_shuffer) return the generated input field name * @param int $varlen the length of the input field name that will be generated * @param string $str_to_shuffer the string that will be used to generate the input field name * @return string the generated input field name */ function get_varname( $varlen = 3, $str_to_shuffer = "abcdefghijklmnopqrstuvwxyz0123456789_" ) { if ( ! $this -> CI -> db_session -> userdata( $this -> key_varname ) ) { $this -> set_token( $varlen, $str_to_shuffer ); } return $this -> CI -> db_session -> userdata( $this -> key_varname ) ; } /** * * @method string get_value($varlen, $str_to_shuffer) return the token * @param int $varlen the length of the input field name that will be generated * @param string $str_to_shuffer the string that will be used to generate the input field name * @return string the token */ function get_value( $varlen = 3, $str_to_shuffer = "abcdefghijklmnopqrstuvwxyz0123456789_" ) { if ( ! $this -> CI -> db_session -> userdata( $this -> key_value ) ) { $this -> set_token( $varlen, $str_to_shuffer ); } return $this -> CI -> db_session -> userdata( $this -> key_value ) ; } // ワンタイムトークンの整合性チェックの中身 /** * * @method bool _is_token_valid() checks if a csrf token is valid * @return bool true if token is valid. false if token is invalid. */ function _is_token_valid() { $result = false; $varname = $this -> get_varname(); $value = $this -> get_value(); if ( $varname ) { if ( isset( $_POST[$varname] ) ) { $result = ( ( strcmp( $value, $_POST[$varname] ) ) == 0 ); } } return $result; } // $_SERVER["REQUEST_URI"]から url_suffixと、末尾の/(の連続)を強制的に取り除いた値を戻す function _get_request_uri() { $request_uri = preg_replace( "#" . $this -> CI -> config -> item( 'url_suffix' ) . "/*$#iu" , "" , $this -> CI -> input -> server( 'REQUEST_URI', true ) ); $request_uri = rtrim( $request_uri, "/" ); return $request_uri; } // suffix指定有りの時は指定された値、指定無しの時はREQUEST_URIを基にワンタイムトークンを生成 function _set_suffix( $suffix="" ) { $this -> token_suffix = ( $suffix ) ? $suffix : $this -> _get_request_uri(); $this -> key_varname = $this -> token_prefix . $this -> key_varname_base . $this -> token_suffix; $this -> key_value = $this -> token_prefix . $this -> key_value_base . $this -> token_suffix; $this -> key_message = $this -> token_prefix . $this -> token_suffix . "_msg" ;//セッションのキーが1次元しか持てないからこんなややこしい事になる } } //endofclass /** * End of file Csrf.php */ /** * Location: ./application/libraries/Csrf.php */ ?>
サンプルソース
(方式A:編集画面とPOST先の遷移URLが違う時を採用)1)コントローラ:application/controllers/tasklist.php
(変更点のみ抜粋し、これまでのソースの一覧画面とかページネーションとかは削っている。
ただしhttp://d.hatena.ne.jp/dix3/20081013/1223862786 の javascriptのバリデーション拡張は入れているまま)
方式Aを使い、edit()→submit()へとPOSTしている。
ここでは、思うところがあって、submit()側でバリデーションエラー時には、
submit側に止めずに、edit()にリダイレクトしてみた。
リダイレクトするとエラーメッセージとか、フォームの値が消えるので、
$this -> db_session -> flashdata() を使って、
一度限りで自動消去されるセッションを利用し、
validationオブジェクトをserializeして保存し、リダイレクト先でunserializeして戻している。
flashdataを使うと、明示的にセッション内のデータを消す必要がないので便利。
(エラー時でもリダイレクトさせずに、 submit()にとどめるか、そもそもedit()→edit()のように自分自身にサブミットする場合は不要)
肝は、submit()内の
if ( !$this -> csrf -> check( "tasklist_edit" ) || $this -> validation -> run() === false ){
で通常のバリデーションチェックの前に、トークンキーのチェックを行っている部分だけ。
application/controllers/tasklist.php
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); // タスクリストというかいろいろ実験 class Tasklist extends Controller { // コンストラクタ function Tasklist() { parent :: Controller(); // ログイン認証、ログインしてなければログイン画面に飛ばされる // $this -> freakauth_light -> check(); // 開発中はscaffoldingを有効に //if ( config_item( 'my_debugger' ) ) { // $this -> load -> scaffolding( 'tasklist' ); // テーブル名とクラス名は同じでなくてもいい //} // urlヘルパ $this -> load -> helper( 'url' ); // フォームヘルパ $this -> load -> helper( 'form' ); // ワンタイムトークンライブラリのロード $this -> load -> library( 'csrf' ); // モデルの読み込み todo:後でモデルに寄せる // $this -> load -> model( 'tasklist_mdl' ); // バリデーションライブラリのロード $this -> load -> library( 'validation' ); $this -> validation->set_error_delimiters('','');//バリデーションエラー埋め込みの<p></p>囲みを解除 } // 登録更新処理、まだ枠だけ // ClientServer Validation を使用した例 // todo:後でxajaxを使用したValidationチェックの実験もやってみる function edit() { $this -> _setup_validation( "edit" ); //バリデーションのセットアップ // リダイレクトされてきたときにフォームの入力値を戻してみる // ページ遷移で /tasklist/submit側でのバリデーションエラー時にリダイレクトさせない場合や、 // /tasklist/editからのsubmit先が /tasklist/editで自分自身の時には必要ない。 if ( $this -> db_session -> flashdata( "tasklist_edit_form" ) ) { $this -> validation = unserialize( $this -> db_session -> flashdata( "tasklist_edit_form" ) ); } $data['title'] = "ClientServer Validationを使ったバリデーションの実験、まだ枠組みだけ、"; // ワンタイムトークンエラーの時のメッセージ、正常時空白 $data['csrf'] = $this -> csrf -> get_message( "tasklist_edit" ); // クライアント側バリデーションのセット // 2番目の引数にコールバックするjavascriptを指定できるようだ $data['validation_js'] = $this -> validation -> javascript( 'なんかエラーがあるじょ' ); // ビューの生成 $this -> _setTpl( "tasklist_edit" , $data ); } // サブミット時の処理(別に名前はsubmitじゃなくても、formのポスト側と合っていればなんでもいい。) function submit() { if ( !$this -> input -> post( "submit" ) ) { // URL直呼びはeditへリダイレクト redirect( "/tasklist/edit/" ); } $this -> _setup_validation( "edit" ); //バリデーションのセットアップ // todo:キャプチャの判定を設置する // todo:呼び出し元編集画面による条件分岐 if ( !$this -> csrf -> check( "tasklist_edit" ) || $this -> validation -> run() === false ) { // バリデーションチェックNGの時はeditの処理をもう一度(とりあえずリダイレクトはしない) // return $this -> edit(); // リダイレクトさせてみる。リダイレクトさせると当然POST値は消えてしまう。 // ただしフォームの入力値を差し戻したいので、 // $this -> validation をシリアライズして flashdataに格納してみた。 $this -> db_session -> set_flashdata( "tasklist_edit_form", serialize( $this -> validation ) ); redirect( "/tasklist/edit/" ); } // バリデーションチェックOKの時の処理 // なんか処理をして完了後リダイレクト redirect( "/tasklist/success/" ); } function success() { $data['title'] = "成功"; $data["msg"] = "うまくいったよん"; $this -> csrf -> clean( "tasklist_edit" ); $this -> _setTpl( "tasklist_success" , $data ); } // ClientServer Validation を使用したバリデーション例 function _setup_validation( $mode ) { // todo:トークンキーの設置 // todo:キャプチャの設置 switch ( $mode ) { case "edit" : // 編集画面のバリデーション // エラー表示用フィールド名のセット // ウチの場合は多言語化は必要ない、 // 多言語対応CMSを作るのなら定数読み込みに変える必要有り $fields = array( 'email' => 'Email', 'password' => 'パスワード', 'passconf' => 'パスワード(確認)', 'firstname' => '氏', 'lastname' => '名', 'city' => '市町村名', 'zipcode' => '郵便番号', 'address1' => '住所1', ) ; // バリデーションルールのセット // Javascriptでの事前バリデーションと共有化される $rules = array( 'email' => 'trim|required|valid_email|xss_clean', 'password' => 'required|min_length[5]|xss_clean', 'passconf' => 'trim|required|matches[password]|xss_clean', 'firstname' => 'trim|required|xss_clean', 'lastname' => 'trim|required|xss_clean', 'city' => 'trim|min_length[2]|xss_clean', 'zipcode' => 'trim|exact_length[7]|xss_clean', 'address1' => 'trim|required|xss_clean', ); break; default: break; } if ( isset( $rules ) ) { $this -> validation -> set_fields( $fields ); $this -> validation -> set_rules( $rules ); } } // ビューの生成 一カ所にまとめた function _setTpl( $viewName, $data = array() ) { // 大枠のテンプレート側の変数(ヘッダ部分のタイトルと、xajax用javascriptタグ) $tpl["page_title"] = "タスクリストにようこそ"; // タスクリスト(実験ソース)のテンプレートにデータをセット $tpl["main_content"] = $this -> load -> view( $viewName, $data , true ); // 大枠のテンプレートに、タスクリストのビューをはめ込む $this -> load -> view( 'base_view', $tpl ); } } //Endofclass /** * End of file tasklist.php */ /** * Location: ./application/controllers/tasklist.php */ ?>
2)中身ビュー:application/views/tasklist_edit.php
http://d.hatena.ne.jp/dix3/20081013/1223862786 のClientServer Validation改 対応とほぼ同一。
違うのは、ワンタイムトークン用に
<?=form_csrf('tasklist_edit');?>
を追加したのと、
トークンエラー表示用に
<?php if(isset($csrf)){ ?> <p class="error"><?=$csrf;?></p> <?php } ?>
を追加したのみ。
application/views/tasklist_edit.php
<h3><?= $title ?></h3> <?=form_open('/tasklist/submit/'); ?> <?=form_csrf('tasklist_edit');?> <fieldset> <legend>クライアント側、サーバ側のバリデーションの共用実験</legend> <!-- 一括でエラーを表示したいとき --> <!-- <p id="error_string" class="none"><?=$this->validation->error_string; ?></p> --> <?php if(isset($csrf)){ ?> <p class="error"><?=$csrf;?></p> <?php } ?> <label>Email<em>*</em></label> <input type="text" name="email" id="email" value="<?=$this->validation->email;?>" size="50" /> <p id="email_error"><?=$this->validation->email_error; ?></p> <label>パスワード<em>*</em></label> <input type="password" name="password" id="password" value="<?=$this->validation->password;?>" size="40" /> <p id="password_error"><?=$this->validation->password_error; ?></p> <label>パスワード(確認)<em>*</em></label> <input type="password" name="passconf" id="passconf" value="<?=$this->validation->passconf;?>" size="40" /> <p id="passconf_error"><?=$this->validation->passconf_error; ?></p> <label>氏<em>*</em></label> <input type="text" name="firstname" id="firstname" value="<?=$this->validation->firstname;?>" size="40" /> <p id="firstname_error"><?=$this->validation->firstname_error; ?></p> <label>名<em>*</em></label> <input type="text" name="lastname" id="lastname" value="<?=$this->validation->lastname;?>" size="40" /> <p id="lastname_error"><?=$this->validation->lastname_error; ?></p> <label>市町村名</label> <input type="text" name="city" id="city" value="<?=$this->validation->city;?>" size="30" /> <p id="city_error"><?=$this->validation->city_error; ?></p> <label>郵便番号</label> <input type="text" name="zipcode" id="zipcode" value="<?=$this->validation->zipcode;?>" size="10" /> <p id="zipcode_error"><?=$this->validation->zipcode_error; ?></p> <label>住所1<em>*</em></label> <input type="text" name="address1" id="address1" value="<?=$this->validation->address1;?>" size="60" /> <p id="address1_error"><?=$this->validation->address1_error; ?></p> <div><input type="submit" onclick="javascript:try{if(!validation_run(this.form)){return false;}}catch(e){window.alert(e);return false;}" name="submit" value="送信" /></div> </fieldset> <?=form_close(); ?>
3)中身ビュー:application/views/tasklist_success.php
4)外側ビュー:application/views/base_view.php
5)cssファイル : ドキュメントルート/css/style.css
変更無しにつき全部省略
頑張るね俺、でも標準で実装されたら意味無くなるな。
(別フレームワークから持ってきた方が賢いような気もするけど、CodeIgniterを使いこなす練習なので気にしないことにする。)
あとはうまく画面遷移と、チケットの発行タイミングとチェックを組み合わせていけば、
何となく対策した気分くらいにはなってくると思う。
さて、明日は何をためしてみようかな。
(08/11/22追記) 改良版を(CodeIgniterの学習 39 - 俺俺改造版csrfライブラリ(ワンタイムトークンCSRF対策)を改良して、一時データをセッションに保持し簡単かつ極力安全に取得消去できるようにする http://d.hatena.ne.jp/dix3/20081111/1226350226)に追加しました。