[CakePHP3] データ漏洩していませんか?RequestHandlerの危険性

予定していたAdventCalendarの記事が間に合いそうになかったので
CakePHPのslackチャンネルで話題にもなったことを記事にしました:kissing_heart:

今回の検証バージョンはCakePHP3.5.7です。
CakePHP 3.1以降のバージョンで発生します。

API実装予定でないWebサービスの場合は特に気が付きにくいので確認してみてください!

今回指摘の箇所はまだリリースされていませんが改善予定です。
しかしこのPRだけではまだ危ないです。
https://github.com/cakephp/app/pull/569

データ漏洩を体験

こんな感じのユーザ一覧ページを作成したいとします。
https://qiita.com/users

CakePHPインストール

cookbookに書いてるとおりにインストールします。

$ composer create-project --prefer-dist cakephp/app app

テーブル作成

usersテーブルを作成。
適当に5件ほどデータを挿入します。

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL,
    password VARCHAR(255) NOT NULL,
    created DATETIME,
    modified DATETIME
);

bake

bakeでModel/Controller/Templateを作成します。

$ bin/cake bake all users

テンプレート編集

ユーザ一覧画面でユーザ名だけ表示したいのでusername以外の項目を削ります

/src/Template/Users/index.ctp
    <table cellpadding="0" cellspacing="0">
        <thead>
            <tr>
                <th scope="col"><?= $this->Paginator->sort('username') ?></th>
            </tr>
        </thead>
        <tbody>
            <?php foreach ($users as $user): ?>
            <tr>
                <td><?= h($user->username) ?></td>
            </tr>
            <?php endforeach; ?>
        </tbody>
    </table>

ユーザ一覧が表示される画面ができます。

index

これで完成?

これであとはデザインを整えるだけ!とするとデータが漏洩してしまします。

データが漏洩しているところを確認してみる

実際にやってみます。
chromeのdevツール以下のようにしてajaxでリクエストを叩いてみます。

var jq = document.createElement('script');
jq.src = "http://code.jquery.com/jquery-3.2.1.min.js";
document.getElementsByTagName('head')[0].appendChild(jq);
$.ajax({
    type: 'GET',
    url: '/users',
    dataType: 'json',
    success: function(response){ console.log(response); },
    error: function(req, err){ console.log(err); }
});

!!
テンプレートでは削除したはずのメールアドレスが表示されてしまいます。

chrome

原因

インストール時点でデフォルトでRequestHandlerコンポーネントが読み込まれています。

このRequestHandlerコンポーネントはAJAXリクエストを自動で判定して_serializeでセットした値をjsonにして返却します。

/src/Controller/AppController.php
    public function initialize()
    {
        $this->loadComponent('RequestHandler');
    }

またAppControllerはリクエストを判定して$this->set()した値をすべて_serializeにしています。

/src/Controller/AppController.php
    public function beforeRender(Event $event)
    {
        // Note: These defaults are just to get started quickly with development
        // and should not be used in production. You should instead set "_serialize"
        // in each action as required.
        if (!array_key_exists('_serialize', $this->viewVars) &&
            in_array($this->response->type(), ['application/json', 'application/xml'])
        ) {
            $this->set('_serialize', true);
        }
    }

そしてbakeで生成したメソッドも_serializeにセットしています。
また、ここの$this->set('_serialize', ['users']);を削除しても上記の影響でserializeされます。

/src/Controller/UsersController.php
    public function index()
    {
        $users = $this->paginate($this->Users);

        $this->set(compact('users'));
        $this->set('_serialize', ['users']);
    }

以上のことからデフォルトのままだとAJAXリクエストをするときにEntityのデータがserializeされjsonで返却されてしまいます。

対策

対策1

RequestHandlerコンポーネントを使わないなら削除します。
とりあえずこれをやっておけば安心です。
これでAJAXリクエストを自動で判定して_serializeでセットした値をjsonにして返却されることはなくなります。

/src/Controller/AppController.php
// $this->loadComponent('RequestHandler');

対策2

RequestHandlerコンポーネントを使いたいときがあると思います。
その場合は以下のようにします。

自動で_serializeにセットされるコードを削除します。

/src/Controller/AppController.php
    public function beforeRender(Event $event)
    {
        // Note: These defaults are just to get started quickly with development
        // and should not be used in production. You should instead set "_serialize"
        // in each action as required.
//        if (!array_key_exists('_serialize', $this->viewVars) &&
//            in_array($this->response->type(), ['application/json', 'application/xml'])
//        ) {
//            $this->set('_serialize', true);
//        }
    }

bakeで生成される$this->set('_serialize', ['users']);を削除する。
本当に_serializeしたい部分だけ記載するようにします。

/src/Controller/UsersController.php
    public function index()
    {
        $users = $this->paginate($this->Users);

        $this->set(compact('users'));
        // $this->set('_serialize', ['users']);
    }

対策3

表示する場合の対策ですが...
表示しない項目はEntityで$_hidden設定することを心がけましょう。
https://book.cakephp.org/3.0/ja/orm/entities.html#id15