cakephp::securityコンポーネントを利用するときの注意

· application

フォームに、ワンタイムトークンを実装することで、リロード対策とCSRF対策が同時に可能です。その実装に、securityコンポーネントという便利なコンポーネントが用意されているのですが、利用の際に嵌ったのでメモ。

なおsecurityコンポーネントを設置して「効きすぎてPOSTできなくなった」というのは見るのですが、「うごかねぇ(リロードできちゃう)」という人向けな内容になります。

まず

一番よくまとまっているのが、こちら

Sun Limited Mt.:CakePHP Security コンポーネントのまとめ

で、参考にさせてもらい設置しました。

ただし、なぜかview側に

<?php echo $html->formTag(); ?>

を記述しなくても、トークンが書き出されました。何故か分かりませんがw

想定

画面遷移ですが、

  • 入力画面(トークンの発行)

  • 完了画面(トークンのチェックOK)

  • リロード

  • 完了画面(トークンのチェックNG(=blackholeメソッド呼び出し))

という予定だったのですが、リロードしてもチェックがOKのままでした。

そこでsecurityコンポーネントのソースを追ってみました。

ここから長いので先に結論から。

(追記:以下の修正はコアコンポーネントを修正しているため非推奨です。このエントリーでそれを解決しています。)

/cake/libs/controller/components/security.php の651行目をコメントアウトして、解決しました。

if ($valid) {
	// $token['key'] = $tokenData['key'];
}

何故そうなったかは、ソースを追って解説していきます。securityコンポーネントは多機能なので、特に今回は関係のあるトークン周りの説明になります。

トークンの発行

まず_generateTokenメソッドでトークンの発行が行われていました。ソースはここ(/cake/libs/controller/components/security.php 620行目)

function _generateToken(&$controller) {
	(・・・省略・・・)
	$authKey = Security::generateAuthKey();
	$expires = strtotime('+' . Security::inactiveMins() . ' minutes');
	$token = array(
		'key' => $authKey,
		'expires' => $expires,
		'allowedControllers' => $this->allowedControllers,
		'allowedActions' => $this->allowedActions,
		'disabledFields' => $this->disabledFields
	);

	if (!isset($controller->data)) {
		$controller->data = array();
	}
	if ($this->Session->check('_Token')) {
		$tokenData = unserialize($this->Session->read('_Token'));
		$valid = (
			isset($tokenData['expires']) &&
			$tokenData['expires'] > time() &&
			isset($tokenData['key'])
		);

		if ($valid) {
			$token['key'] = $tokenData['key'];
		}
	}
	$controller->params['_Token'] = $token;
	$this->Session->write('_Token', serialize($token));
	return true;
}

細かく見ていきます。

$authKey = Security::generateAuthKey();
	$expires = strtotime('+' . Security::inactiveMins() . ' minutes');
	$token = array(
		'key' => $authKey,
		'expires' => $expires,
		'allowedControllers' => $this->allowedControllers,
		'allowedActions' => $this->allowedActions,
		'disabledFields' => $this->disabledFields
	);

	if (!isset($controller->data)) {
		$controller->data = array();
	}

こまごまありますが、大事なのは、$expiresにSecurity::inactiveMins()が設定されていること(後述)で、app/config.php のsecurity.levelによって値が変わります。(今回はどの値でも問題がありましたw)

$tokenData = unserialize($this->Session->read('_Token'));

$tokenDataにセッションの_Tokenキーが(アンシリアライズされて)セットされます。

$valid = (
			isset($tokenData['expires']) &&
			$tokenData['expires'] > time() &&
			isset($tokenData['key'])
		);

$tokenData[’expires’]と今の時間を比較し、その結果を$validにセット

if ($valid) {
			$token['key'] = $tokenData['key'];
		}

$validがtrueなら、$token[‘key’]を$tokenData[‘key’]で上書きしています。つまり、

有効期限が今より先なら、保持していたセッションのkeyで上書きしています。

$controller->params['_Token'] = $token;
	$this->Session->write('_Token', serialize($token));

で、$tokenでセッションの_Tokenをセットしています。

トークンのチェック

トークンのチェックは_validatePostメソッドで実行されてるようで、(540行目~)

function _validatePost(&$controller) {
	(・・・省略・・・)
	$data = $controller->data;

	if (!isset($data['_Token']) || !isset($data['_Token']['fields']) || !isset($data['_Token']['key'])) {
		return false;
	}
	$token = $data['_Token']['key'];

	if ($this->Session->check('_Token')) {
		$tokenData = unserialize($this->Session->read('_Token'));

		if ($tokenData['expires'] < time() || $tokenData['key'] !== $token) {
			return false;
		}
	}
	(・・・省略・・・)
}

細かく見ていくと、まず,

function _validatePost(&$controller) {
	$data = $controller->data;

	if (!isset($data['_Token']) || !isset($data['_Token']['fields']) || !isset($data['_Token']['key'])) {
		return false;
	}
	$token = $data['_Token']['key'];

$tokenに$controller->dataの[’_Token’][‘key’]キーがセットされています。つまりPOSTで飛んでくるトークンですね。

if ($this->Session->check('_Token')) {
		$tokenData = unserialize($this->Session->read('_Token'));

		if ($tokenData['expires'] < time() || $tokenData['key'] !== $token) {
			return false;
		}
	}

セッションにトークンがあれば、それを読んで$tokenDataに(アンシリアライズして)セットします。で、

$tokenData[’expires’]が今より前か、または、$tokenData[‘key’]が$tokenと一致しなければ、falseが返ります。

で、falseが返るとblackHoleメソッドが呼ばれる流れになってます。

で、何故動作しない(↑でfalseにならない)のか

_validatePostメソッドで比較しているトークン値は、

  • 入力画面から飛んできたPOSTのトークン

  • sessionのトークン

なので、リロードした際にはPOSTのトークンは同じ値が飛んできて、sessionのトークンが変わっていればいいはずです。

しかし、_generateTokenメソッドでは、変わってほしいsessionのトークンがexpiresとtime()の比較の結果、今のsessionのトークンで上書きされてしまいます。なので、expiresで設定された時間(たとえば10分)を過ぎてから、リロードすれば期待通りの動きになりますが、その期間内のリロードはすべて受け入れる形になってました。。。なんで。。。

再び結論

ということで、一番影響なさそうな、以下の部分(sessionトークンを上書きする部分)をコメントアウトして、対応しました。

if ($valid) {
	// $token['key'] = $tokenData['key'];
}

うーん、使い方が間違ってるんですかね?w こんなことしないと使えないとは思えない。。。

もっと普通のw方法があればぜひ教えて下さい。。。

画像未復旧: hatena.gif

関連があるかもしれないエントリー