k-holyのPHPとか諸々メモ

Webで働くk-holyがPHP(スクリプト言語)とか諸々のことをメモしていきます。ソースコードはだいたいWindowsで動かしてます。

RedBeanで見るMass Assignment

Railsの仕様(と言っていいのかな?)に起因する、GitHubのMass Assignment脆弱性が狙い撃ちされたのが話題になりましたが、ORMライブラリRedBeanの、RedBean_OODBBean::import()がそのまんま、そういう実装だったのを思い出したので確認してみました。

You can import an array into a bean using: 

    $book->import($_POST);

 The code above is handy if your $_POST request array only contains book data. It will simply load all data into the book bean. You can also add a selection filter: 

    $book->import($_POST, 'title,subtitle,summary,price');

 This will restrict the import to the fields specified. Note that this does not apply any form of validation to the bean. Validation rules have to be written in the model or the controller.

http://www.redbeanphp.com/manual/import_and_export より引用

このサンプルコードだけでも一目瞭然なのですが、該当部分のRedBean_OODBBean::import()のコードはこうなってます。

    public function import( $arr, $selection=false, $notrim=false ) {
        if (is_string($selection)) $selection = explode(",",$selection);
        //trim whitespaces
        if (!$notrim && is_array($selection)) foreach($selection as $k=>$s){ $selection[$k]=trim($s); }
        foreach($arr as $k=>$v) {
            if ($k!='__info') {
                if (!$selection || ($selection && in_array($k,$selection))) {
                    $this->$k = $v;
                }
            }
        }
        return $this;
    }

https://github.com/gabordemooij/redbean/blob/master/RedBean/OODBBean.php より引用

第2引数で配列またはカンマ区切りで、取り込むフィールドを指定できるようになってるんですね。
モデルに実装したメソッドを呼べるので勘違いしそうですが、OODBBean自身はモデルへのアクセス機能を持った配列オブジェクトみたいなものです。

OODBBeanにセットされた値は、RedBean_Facade::store()経由で実行されるRedBean_OODB::store()に渡されて保存されるのですが、このメソッド、RedBean_Facade::freeze(true)しておかないと勝手にカラムが追加されてしまう危険な仕様になっています。
そのため、上記のRedBean_OODBBean::import()で第2引数で取り込むカラム名を指定せず$_POSTを直接渡していると、好きなようにカラムを追加されてしまうという恐ろしいことになります。

恥ずかしながら、ソースを追ったわけではなく(追おうとしたけど、200行近いメソッドのコード内で更に他のメソッドが次々と呼ばれてたので早々に挫折しました…)、フォームのname属性をtypoしたせいでSQLiteのテーブルにカラムが追加されてしまって、初めてこの仕様に気付きました。
Mass Assignmentという言葉は知らなかったのですが、この時の失敗のおかげで、RedBean_Facade::freeze()の意味とRedBean_OODBBean::import()の第2引数の重要性を理解できたわけです。

ちなみに今の自分のコードでは、RedBean_SimpleModelを継承したモデルに操作対象テーブルのフィールド名を定義し、それを使ってRedBean_OODBBean::import()の第2引数を指定しています。

AbstractModelクラスから該当部分を抜粋

abstract class AbstractModel extends \RedBean_SimpleModel
{
    protected $fields = array();
    public function getFieldNames()
    {
        return array_keys($this->fields);
    }
}

ProfilesModelクラスから該当部分を抜粋

class ProfilesModel extends AbstractModel
{
    protected $fields = array(
        'surname' => '苗字',
        'name'    => '名前',
        'notes'   => '備考',
        'year_of_birth' => '生年',
        'year_of_death' => '没年',
    );
}

呼び出し元のスクリプトから抜粋

    $profile = \R::dispense('profiles');
    switch($request->getMethod()) {
    case 'POST':
        $profile->import($request->request->all(),
            $profile->getFieldNames());
        if ($request->get(CSRF_TOKEN_NAME) === $sessionId) {
            try {
                \R::store($profile);
            } catch (InputValidationException $ex) {
                $app['phptal']->set('errors', $ex->getErrors());
                break;
            }
            $app['session']->setFlash('message', '人物情報を登録しました。');
            return $app->redirect('/profiles/', 303);
        }
        break;
    }

こういった方法で、モデルクラス経由で更新されたくないカラムの値を保護することはできるのですが、たとえば管理画面と一般利用者画面、あるいは利用者の権限によって操作できるカラムを制限したいケースには対応できません。
アプリケーションフレームワークがなかった頃、リクエスト変数を操作対象となる配列にセットするコードを画面毎にズラズラと書いていましたが、そういう方法ではこのような問題は起こりえないのですが…。
DRYを突き詰める過程で、今まで明示的にやってたことをやめようという時は、不備がないのかよくよく考えないといけません。

またこの件に関連して、バリデーションをModelかFormのどちらでやるか?といった話も目にしました。
自分は業務では今のところ、フォームと対になったコントローラでは受け入れるフィールドの定義とモデル用の値への変換に留めて、バリデーションはモデルで一括して行う派です。
(と言っても、いわゆるドメインモデルではなく、URL別のページスクリプト→ユースケース別のコントローラー→ユースケース別のトランザクションスクリプト→CRUD&共通バリデーションを行うクラス→DAOでひたすら配列をリレーするという旧型な構造で作ってて、設定ファイル的なものも使っていないのですが)

このあたりは以前からずっとモヤモヤ考えてて、複数の層にまたがって実行されるバリデーション処理と、バリデーション結果からユーザーへの通知メッセージを生成する処理をどうやって分けるかが課題になってます。

なお、GitHubのMass Asssignment脆弱性については、これらの記事で把握しました。
github の mass assignment 脆弱性が突かれた件 - blog.sorah.jp
Git Hubの脆弱性とMass Asssignment