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


はじめに

普通のフォームの場合は殆どおこらないが、選択肢によりフォームの表示切り替えをしたり、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ヘルパーを利用して下さい。

最後に

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