のんラボ

CakePHP3.0でCSRF対策済みのアクションテストを行う

2019/11/28 2019/12/12 CakePHP3.0でCSRF対策済みのアクションテストを行う

CakePHP「3.0」で「CSRF」のコントローラー「テスト」を行う

こんにちはNonです。
今回はあるプロダクトの開発に出くわしたちょっとした内容の課題を解決する手法についてお話しようと思います。
かなりニッチな需要となりますので、お役に立てるかわかりませんが参考になれば嬉しいです。

はじめに

環境は

  • CakePHP「3.0」

です。CakePHP「3.1」以降ではCSRF対策のテスト手法は確率されているので、この記事の対象者はCakePHP3.0のユーザーということになります。

結論

CakePHP3.0では

copied./**
 * Test sample method
 */
public function testSample()
{
    // sample-tokenの部分は任意の値で大丈夫です。
    $this->cookie('csrfToken', 'sample-token');
    $url = Router::url(['controller' => 'Sample', 'action' => 'sample']);
    $data = [
        'sample' => 'サンプル',
        '_csrfToken' => 'sample-token'
    ];
    $this->post($url, $data);
}

sample-tokenの部分は任意の値で大丈夫です。

CakePHP3.1以降では

copied./**
 * Test sample method
 */
public function testSample()
{
    $this->enableCsrfToken();
    $this->enableSecurityToken();
    $url = Router::url(['controller' => 'Sample', 'action' => 'sample']);
    $data = [
        'sample' => 'サンプル',
    ];
    $this->post($url, $data);
}

事例

とあるプロダクトの開発中、こんな出来事に遭遇しました。

あれ?このプロダクトCSRF対策されてないやん……

このプロダクトは簡素なもので、最近引き継いだばかり、プロトタイプと言っていいほどの機能にしか持っていないので、当時の作成者は面倒臭くてCSRF対策を忘れていたのでしょう。

よっしゃ、CakePHP3だしCSRF対策楽勝やろ!やったろ!

軽い気持ちで実装しました。

問題

CSRF対策は導入できたけどPHPUnit通らなくなってもうた……

CakePHP3の更新HPCSRFアクションのテストの項目では……

CsrfComponent や SecurityComponent で保護されたアクションのテスト
SecurityComponent または CsrfComponent のいずれかで保護されたアクションをテストする場合、 テストがトークンのミスマッチで失敗しないように自動トークン生成を有効にすることができます。

copied.public function testAdd()
{
    $this->enableCsrfToken();
    $this->enableSecurityToken();
    $this->post('/posts/add', ['title' => 'Exciting news!']);
}

また、トークンを使用するテストで debug を有効にすることは重要です。SecurityComponent が 「デバッグ用トークンがデバッグ以外の環境で使われている」と考えてしまうのを防ぐためです。 requireSecure() のような他のメソッドでテストした時は、適切な環境変数をセットするために configRequest() を利用できます。

copied.// SSL 接続を装います。
$this->configRequest([
    'environment' => ['HTTPS' => 'on']
]);

バージョン 3.1.2 で追加: enableCsrfToken() と enableSecurityToken() メソッドは 3.1.2 で追加されました。

あれ?あかんやん

そら通らんわ。Cakeのソース追ってもenableCsrfToken()enableSecurityToken()も通りで無いわけや……

解決

でもよく考えて見ると、CSRFってトークン送信してそのトークンが合ってるだけで良い訳で、しかもPHPUnitでのリクエストが送信されれば良い(CSRF対策のテストがしたいわけではない)ので、普通にトークン送信すれば良いんじゃない?

ということで、やってみました。

CakePHP3の仕様では、HTML側に埋め込まれるCSRFトークンは

copied.<input type="hidden" name="_csrfToken" value="43b804c28f3bd305a513c7c8ba1f1ce61e5ac6a9">

こんな感じなので、PHPUnitでリクエストするときは

copied.$this->post($url, [
    '_csrfToken' => '43b804c28f3bd305a513c7c8ba1f1ce61e5ac6a9',
])

という形式になっていれば、OK。

この送信したトークンをサーバー側のクッキーと比較するはずなので、同様にPHPUnitでは

copied.$this->_cookie('csrfToken', '43b804c28f3bd305a513c7c8ba1f1ce61e5ac6a9');

こう。

ということで最終的に下記のようなテストコードに。

copied./**
 * Test sample method
 */
public function testSample()
{
    // sample-tokenの部分は任意の値で大丈夫です。
    $this->cookie('csrfToken', 'sample-token');
    $url = Router::url(['controller' => 'Sample', 'action' => 'sample']);
    $data = [
        'sample' => 'サンプル',
        '_csrfToken' => 'sample-token'
    ];
    $this->post($url, $data);
}

結果

無事成功しました。

余談

CakePHP3.1.2以降では……

copied./**
 * Test sample method
 */
public function testSample()
{
    $this->enableCsrfToken();
    $this->enableSecurityToken();
    $url = Router::url(['controller' => 'Sample', 'action' => 'sample']);
    $data = [
        'sample' => 'サンプル',
    ];
    $this->post($url, $data);
}

これでOKです。

設定するの面倒なので、CakePHPのバージョンあげますか……

最後に

フレームワーク独特の課題にぶつかりまして、少し楽しかったです。
実はもうCakePHP3のバージョンアップソースは完成しているので、またテスト内容をリライトしなければならないのですが、CakePHP3.0特有の課題を見つけることができてよかったと思います。

地味な内容になってしまいましたが、今後もニッチな内容のものを見つけましたら記事にしていこうと思います。

その時はよしなに。

.

おまけ

https://labo.nozomi.bike
僕の個人ブログです。(Laravel + Vue)で自作。
こっちにしかない記事も多数ありますので、よかったら見てやってください。