【CakePHP2.x】SecurityComponent の落とし穴

# はじめに
Mass Assignment対策やCSRF対策を行う上での気づきをまとめました。
SecurityComponent については公式ドキュメントをご確認ください。
https://book.cakephp.org/2.0/ja/core-libraries/components/security-component.html

# パラメータ無しのPOSTは通過してしまう
SecurityComponent による検証は、formパラメータが1つ以上あって初めて機能するようです。
例えば、以下のようなフォームを送信すると blackHole にはならず、通常どおりアクションが実行されてしまいます。

```html+php:edit.ctp
<form action="/user/edit" id="UserForm" method="post" accept-charset="utf-8">
  <input type="submit" value="送信">
</form>
```

```php:UsersController.php
App::uses( 'AppController', 'Controller' );

class UsersController extends AppController {

  public $components = [
    'Auth',
    'Security',
  ];

  public function edit() {
    if ( $this->request->is( 'post' ) ) {
      $this->User->id = $this->Auth->user( 'id' );

      if ( $this->User->save( $this->request->data ) ) {
        $this->Flash->success( '保存成功' );
        $this->redirect( '/' );
      }

      $this->Flash->error( '保存失敗' );
    }
  }
}
```

パラメータが無いため当然 save() には失敗しますが、パラメータが無いこと自体想定しておらず不正として扱うべきなので、本来は何も実行したくありません。

以下のようにリクエストのチェックを挟んで、これを回避できます。

```php:UsersController.php
  public function edit() {
    if ( $this->request->is( 'post' ) ) {

      $user = $this->request->data['User'] ?? null;

      if ( !$user ) {
        throw new BadRequestException();
      }

      $this->User->id = $this->Auth->user( 'id' );

      if ( $this->User->save( $this->request->data ) ) {
        $this->Flash->success( '保存成功' );
        $this->redirect( '/' );
      }

      $this->Flash->error( '保存失敗' );
    }
  }
```

# FormHelper を使わなければ有効にならない
これは公式ドキュメントにも記載があります。
トークンが生成されないので当たり前ですね。

```html+php:edit.ctp
<form action="/user/edit" method="post" accept-charset="utf-8">
  <input type="text" name="data[User][email]" value="hoge@example.com">
  <input type="submit" value="送信">
</form>
```

FormHelperを使えば以下のようなhidden要素が自動生成されます。

```html+php:edit.ctp
<?php
  $this->Form->create( 'User', [
    'url' => ['controller' => 'Users', 'action' => 'edit'],
  ] );

  $this->Form->end( '送信' );
?>

<!--
<form action="/user/edit" id="UserForm" method="post" accept-charset="utf-8">
  <input type="hidden" name="_method" value="POST">
  <input type="hidden" name="data[_Token][key]" value="[トークン文字列]" id="TokenXXXXXXXXXX">
  <input type="submit" value="送信">
  <input type="hidden" name="data[_Token][fields]" value="[フォーム構成要素のハッシュ文字列]" id="TokenFieldsXXXXXXXXXX">
  <input type="hidden" name="data[_Token][unlocked]" value="" id="TokenUnlockedXXXXXXXXXX">
</form>
-->
```

# select, radio, checkbox の検証は信用しすぎない
これも公式ドキュメントに記載があります。

```html+php:edit.ctp
<?php
  $this->Form->create( 'User', [
    'url' => ['controller' => 'Users', 'action' => 'edit'],
  ] );

　　　　$this->Form->input( 'gender', [
    'type' 　　　　　　=> 'radio',
    'options' => [　0 => '男',　1 => '女'　],
    'default'　　=> 0,
  ] );

  $this->Form->end( '送信' );
?>

<!-- 以下が生成される -->
<input type="hidden" name="data[User][gender]" id="UserGender_" value="">

<input type="radio" name="data[User][gender]" id="UserGender1" value="0">
<label for="UserGender1">男</label></li>

<input type="radio" name="data[User][gender]" id="UserGender2" value="1">
<label for="UserGender2">女</label></li>
```

例えば、生成された input要素の `value="0"` を `value="9"` に変更後 POST しても改ざん検知されません。

対策としては、Model側のバリデーションをきっちり定義しておくことが重要です。
保存処理の前に、値をアレコレする場合は自前でチェックしておけばよいと思います。

# SecurityComponent + fieldList でさらに堅牢にする
ここまでガチガチにする必要があるか分かりませんが、`Model::save()` の引数には `$fieldList` というものがあります。

[Model::save(array $data = null, boolean $validate = true, array $fieldList = array())](https://book.cakephp.org/2.0/ja/models/saving-your-data.html#model-save-array-data-null-boolean-validate-true-array-fieldlist-array)

`$fieldList` 第3引数には、保存を許可するフィールド名の配列を渡します。

許可されたフィールド以外は、実際に `Model::save()` に値が渡ったとしても保存されません。
Model にリストを定義しておけば管理しやすいと思います。

```php:UserModel.php
class UserModel extends AppModel {

  // 中略

  /**
   * 保存許可フィールドリストを返す
   * @param  [string] $action
   * @return [array]
   */
  public function allowFields( $action ) {
    if ( array_key_exists( $action, self::ALLOW_FIELDS ) ) {
      return self::ALLOW_FIELDS[ $action ];
    }
    throw new OutOfBoundsException( 'キーが不正です' );
  }

  private const ALLOW_FIELDS = [
    'edit' => [
      'email',
      'last_name',
      'last_name_kana',
      'first_name',
      'first_name_kana',
      'updated_at',
    ],
  ];
}
```

```php:UsersController.php
  public function edit() {

    // 中略

    $saved = $this->User->save(
      $user, true,
      $this->User->allowFields( $this->action )
    );

    if ( $saved ) {
      $this->Flash->success( '保存に成功しました' );
      $this->redirect( '/user/home' );
    }

    $this->Flash->error( '保存に失敗しました' );
  }
```

SecurityComponent を利用している場合は恩恵が少ない（？）と思いますが、
利用せずにセキュリティを確保するには保存許可フィールドを明示するべきです。

# おわりに
CakePHP初心者なので「こうすればもっと良いよ」とか改善点や補足のコメントいただけると嬉しいです。