Hatena::ブログ(Diary)

24時間CakePHP このページをアンテナに追加 RSSフィード

CakePHP1.2.8以前、1.3.5以前に重大なセキュリティホールが見つかりました。
ただちにコアを最新版にアップデートすることをお勧めします。

参考: CakePHPのSecurityComponentに深刻なセキュリティホールが見つかりました - Shin x blog

2010/06/10

気絶するほど簡単な自動ログインの実装

注意

この実装はクッキーにユーザ名とパスワードを保持させていますが、パスワードを保持させるのは大変危険なので、実際のアプリケーションで動かす場合は時限つきAuthorizeトークンを発行してそれを保持させる実装にするなど、クッキー盗聴対応を必ずしましょう。

トークンを使う実装に修正しました。(16:25)

この実装は、クッキー盗聴対策のため、トークンを発行し、それをクッキーに保存します。

CakePHPクッキーコンポーネントは賢く、Security.ciperSeedというキーを元に復号可能な暗号化をクッキーに対し施しているため、直に読めることはないのですが、それでも解析されたら丸見えになります。これを避けるため、パスワードを直接保存することがないようにしましょう。

ワンタイムトークンを使う実装に修正しました。(18:51)

いつも同じトークンを発行してしまうと、再生(リプレイ)攻撃の脆弱性が起こりうるためです。(thanks to @yohwaki)

参考

準備

hack_pluginプラグインとして配置。

AuthコンポーネントにAppAuthをエイリアスマッピングしちゃいます。
<?php
class AppController extends Controller{
	$components = array('Auth', 'Hack.Alias' => array('Auth' => 'AppAuth'));
}
Userモデルにトークンを返すメソッドを作る

特定の条件で同じトークンを発行するメソッドを作っておきます。

トークンの発行・トークンからのユーザーの検索をするメソッドを作っておきます。

今回はtoken()というメソッド名とします。

※生成するトークンはランダムかつ期限付きにするようにしないと、再生攻撃による脆弱性の恐れがあります。

引数を渡すと、トークンとして扱われそれをもとにユーザーIDを返し、何も指定しないとトークンを発行・ユーザモデルに保存してそれを返すというものです。

実装

"remenberme"といったキーのクッキーをもっていたら、自動ログインを試みます。

明示的にログアウトするときはこのクッキーを削除します。

UsersControler
<?php
class UsersController extends AppController {
	var $name = 'Users';
	var $components = array('Cookie');

	function login(){
		if ($this->Auth->user()) {
			// redirect to somewhere
		} else {
			if ($token = $this->Cookie->read('rememberme')) {
				$user_id = $this->User->token($token);
				if ($user_id) {
					if ($this->Auth->login($user_id)) {
						// AuthComponent::redirect() は自動リダイレクトのURLを返す
						$this->redirect($this->Auth->redirect(), null, true);
					}
				} else {
					$this->Cookie->delete('rememberme');
				}
			}
		}
	}

	// 明示的にログアウト
	function logout(){
		if ($this->Cookie->check('rememberme')) {
			$this->Cookie->delete('rememberme');
		}
		$this->redirect($this->Auth->logout());
	}
}
Authコンポーネント継承したAppAuthコンポーネントを作ります。
<?php
// App/controllers/components/app_auth.php
App::import('Component', 'Auth');

class AppAuthComponent extends AuthComponent{
	// クッキーコンポーネントを追加
	function __construct(){
		$this->components[] = 'Cookie';
		parent::__construct();
	}

	// コントローラへの参照を保持
	function initialize(&$controller, $settings = array()){
		parent::initialize($controller, $settings);
		$this->Controller =& $controller;
	}

	function login($data = null){
		$result = parent::login($data);
		// ログイン成功
		if($result){
			if(!empty($this->Controller->data['User']['rememberme'])){
				$token = $this->getModel()->token();
				// 2週間自動ログインのためのクッキーを保存
				$this->Cookie->write('rememberme', $token, true, '+2 weeks');
			}
		}
		return $result;
	}
}

補足

ちなみに、Hackプラグインを使わないでUsersController::login()でこのような処理を実装しようとすると、Authコンポーネントのstartup()メソッドをトレースしなくてはならず、非常に汚くなります。

#修正前に比べると気絶するほど簡単・・・か?ってほど長くなりましたが、ご愛嬌ということで :D

おまけ

Userモデルの例

bakeryのソースを参考にしています。

  • ワンタイムトークンを使う方式
    • auto_loginsという別テーブルを使う
    • カラムはid, user_id, expires, token
<?php
class User extends AppModel {
	var $hasMany = array(
		'AutoLogin',
	);
	
	function token($token = null) {
		if ($token !== null) {
			$conditions = array(
				'AutoLogin.token' => $token,
				'AutoLogin.expires > '=> date('Y-m-d H:i:s'),
			);
			return $this->AutoLogin->field('user_id', $conditions);
		}
		
		if($this->id){
			$data = array(mt_rand(), mt_rand() => mt_rand());
			$token = Security::hash(serialize($data), null, true);
			$expires = '+2 weeks';
			
			$data = array(
				'AutoLogin' => array(
					'user_id' => $this->id,
					'token' => $token,
					'expires' => date('Y-m-d H:i:s', strtotime($expires)),
				)
			);
			$this->AutoLogin->create($data);
			$this->AutoLogin->save();
			return $token;
		}
		
		return null;
	}
}

ユーザモデルのカラムとしてトークン、トークンの存在期間を設けるのも一つの手です。(副作用として、複数のブラウザPCから自動ログインすることができなくなります)

login.ctp
<?php
	echo $this->Form->create('User', array('legend' => __('Login',true)));
	
	$fields = array(
		'username' => array('label' => __('User name', true)),
		'password' => array('label' => __('Password',true)),
		'rememberme' => array('type' => 'checkbox', array('label' => __('Remember me',true))),
	);
	echo $this->Form->inputs($fields, array('legend' => __('Login',true)));
	
	echo $this->Form->submit(__('Login', true));
	echo $this->Form->end();
?>

初心者 初心者  2010/11/03 21:13 ・気絶するほど簡単な自動ログインの実装での質問です、
hack_pluginをダウンロードしたらどこに配置すればいいのでしょうか?
またデータベースはどのようなものを用意すればいいのでしょうか?
お時間がありましたらご回答お願い致します。

hiromi2424hiromi2424 2010/11/04 23:11 plugins/hack/以下に配置してください。
plugins/hack/controllers/...
plugins/hack/views/...
というふうです。

テーブルに関しては、CakePHPの認証について基本的なものの他は、必須のものはありません。

この記事は初心者向けには難解ですので、入門段階ではお勧めできません。不親切な点はご了承ください。

おじじおじじ 2011/01/18 23:10 Parse error: syntax error, unexpected T_VARIABLE, expecting T_FUNCTION in C:\xampp\xampp\htdocs\cakephp136\salon\app\app_controller.php on line
のエラーが
$components = array('Auth', 'Hack.Alias' => array('Auth' => 'AppAuth'));

のところででるんですけど・・・文法的におかしくないし・・・いったい??????
なんなんでしょうかねぇ〜〜????

hiromi2424hiromi2424 2011/01/18 23:19 >文法的におかしくないし
ダウト。
varが抜けてると推測してみます。

おじじおじじ 2011/01/21 10:11 なるほど^^PHPがまだわかってないみたいでしたw
で・・・質問ですが・・login.ctpとかは各テーブルのフォルダの中に置くと解釈したんですが、いいのでしょうか?
あとWarning (2): Cannot modify header information - headers already sent by (output started at C:\xampp\xampp\htdocs\cakephp136\salon\app\controllers\components\app_controller.php:2)
のエラーがでるのですが・・・わかりません!おたすけを^^
app_controller.phpはAPPの直下のほうがいいのでしょうか?
初心者ですがなんとか使わせて頂こうとしています^^

おじじおじじ 2011/01/23 06:15 ↑これだけで回答出来るわけないし・・・私でもむりだww
で・・情報が欲しいのですが・・LOGINはindexの前に走ったら、indexにはいかないんですよね??(同時に)
別にloginでechoとかかけてないのですが・・・
コントローラーでindexの中でPOSTの変数を参照してますが、index単体ではうまく動いてましたので・・

junichi_11junichi_11 2011/02/10 18:47 HackPluginの作成ありがとうございます。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/hiromi2424/20100610/1276147639