Edited at

CakePHP2.x フォームのPOSTでCSRF判定されハマったときに確認すること

More than 1 year has passed since last update.


はじめに

普通のフォームの場合は殆どおこらないが、選択肢によりフォームの表示切り替えをしたり、AjaxでPOSTさせたりした場合に、POSTしたのに、リダイレクトされてしまい、うまく動かない!なんでだ!とハマってしまったときに確認する事項を紹介します。


ハマる原因はなにか?


SecurityComponent と CSRF

POSTしたのにリダイレクトされるのは、SecurityComponentの仕業です。なぜそうなるかというと、SecurityComponentが「このPOSTリクエストはCSRFなので、不正だ!」と判定しているのです。

CSRFについては、詳しい人がいると思うのでその人に任せますが、簡単に言うと、HTMLやリクエストの改ざんにより、サイト運営者が意図しないデータをPOSTすることで、意図しないデータの保存や不具合を発生させる攻撃です。

一見やなSecurityComponentですが、実はとてもいいやつなのです。誤解しないでください。


SecurityComponentはどういうチェックをしているか?

POSTされた データの中に、_Token という配列データが含まれるのはご存知でしょうか?

これが重要なのですが、SecurityComponentはこの _Token 内の fieldskey をチェックしています。

POSTされたデータのフィールドと値を元に、ハッシュ値などを生成し、POSTされたfields値とkey値の値が一致しているかチェックしています。

該当箇所は以下

// lib/Cake/Controller/Component/SecurityComponent.php

protected function _validatePost(Controller $controller) {
$token = $this->_validToken($controller);
$hashParts = $this->_hashParts($controller);
$check = Security::hash(implode('', $hashParts), 'sha1');
if ($token === $check) {
return true;
}
$msg = self::DEFAULT_EXCEPTION_MESSAGE;
if (Configure::read('debug')) {
$msg = $this->_debugPostTokenNotMatching($controller, $hashParts);
}
throw new AuthSecurityException($msg);
}

実際のソースはこれ

で、POSTされたデータに_Tokenを入れてるのは、 FormHelper がやっております。

という前提の元、ハマっている場合、以下を確認すると解決できる(はずです)。


チェックリスト


POSTのフィールドと初回表示のフォームのフィールドが同じか?

選択肢により、フォームが変わったりすることがある場合は CSRF 判定されてしまうので、入るかわからないフィールドはコントローラの beforeFilter() などで、SecurityComponentdisabledFields プロパティに設定する必要があります。

function beforeFilter()

{
parent::beforeFilter();
$this->Security->disabledFields = array('ModelName.fieldName1', 'ModelName.fieldName2');
}

disabledFields プロパティに設定すれば、SecurityComponentのチェックの際に該当フィールドを無視してチェックしてくれるので、CSRF判定されません。

※ 注意 ※

ただし、該当フィールドは悪意の第三者に任意の値をPOSTされるリスクを許容するという設定になるので、POSTの値を信じず、必ずバリデーション等値のチェックを行ってください。


_Token をPOST値に含めてるか?

AjaxなどでPOSTする値を明示的に指定し直している場合、_Token の値を指定し忘れると CSRF 判定されてしまうので、明示的に指定してください。

$.ajax({

type: 'post',
url: 'URL',
dataType: 'json',
data: {
ModelName: {
fieldName1: $('#ModelNameFieldName1').val(),
fieldName2: $('#ModelNameFieldName2').val(),
},
// ※ これを指定する必要ありますよ
_Token: {
// もっと素敵なやり方ある気がするので詳しい方教えてください。
fields: $('input[name="data[_Token][fields]"]').val(),
key: $('input[name="data[_Token][key]"]').val(),
unlocked: $('input[name="data[_Token][unlocked]"]').val(),
}
},
// 以下省略


Formヘルパーを使ってるか?

HTMLで直接記載している場合、Formヘルパーが fields値とkey値を作る場合に計算されないので、SecurityComponentがチェックするのと差異が出るためCSRF判定されてしまうので、必ずFormヘルパーを利用して下さい。


最後に

僕自身このケースでハマること合計数十時間。コアライブラリのコードを読んでスッキリ理解してハマることがなくなりました。参考になれば嬉しいです。