極める!Security Component (CakePHP Advent Calendar 2010 24日目)
この記事はCakePHP Advent Calendar 2010 24日目に向けて書きました
CakePHP Advent Calendar 2010に参加したい!と思ったときには既に遅し、トリの24日しか空いておらず、トリっぽい記事なんて書けないわーと思っていました。が、主催のcakephperさんより「べつにトリっぽくなくていいですよ」との温かい言葉を頂いたので、前々から書きたかったSecurityコンポーネントの話を書いてみます。対象バージョンは1.3.6 Stableです。
ちなみにAdvent Calendarとはなんぞや?といいますと、cakephperさんの書き込みより以下引用。
技術系の方がやっているAdvent Calendar(アドベントカレンダー)は、12/1からクリスマスまでに1日ごとにその技術に関する何かしらの記事を書いて、アップしていくというもので、担当日を決めて何人かでどんどん記事をアップしていくパターンが多いです。
というものです。
僕の前の23日目はyashioさんの【CakePHP】メディアビューの使い方(CakePHP Advent Calendar 2010) Ooooooops!でした。記事一覧については、CakePHPフォーラムのAdvent Calendarスレッドに記事ごとにコメントが投稿されていますのでぜひ御覧ください。
Security Componentはハマり所
Securityコンポーネントは最もハマり所となりうる箇所の一つです。動作を理解していないと不可解な空白ページが出力されたりして、デバッグに多大な時間を費やすことが多いです。ハマらなければ便利なのですが...。
パラメータ改ざん対策でハマる
Securityコンポーネントは多くの機能を備えています。
- CSRF対策
- パラメータ改ざん対策
- ログインチェッカ
- リクエストメソッド(GET/POST等)チェッカ
- SSL接続チェッカ
- Basic認証
- Digest認証
この中で突出してハマるのが、パラメータ改ざん対策です。そして、パラメータ改ざん対策はCSRF対策と同時に動作するので、CSRFだけを利用することができません。このため、あまりのハマりっぷりに嫌気が差したのか、「CSRF対策をしたいだけなので、Securityコンポーネントの利用をやめて自作した」という実例があるくらいだったりします。(ただしCSRF対策の実装も一長一短なので、パラメータ改ざんは関係ないかもしれません)
Security Componentの基本動作
Securityコンポーネントを読み込むと、POSTメソッドのリクエストに対してCSRF対策とパラメータ改ざん対策がデフォルトで動作します。CSRF対策では、Formヘルパーで作られたフォーム画面以外(悪意のあるサイト等)からのPOSTを弾き、パラメータ改ざん対策ではFormヘルパーで正しく作られたフォーム部品(input, select等)以外からのPOSTを弾くようになります。
このどちらかで弾かれた場合は、デフォルトでは空白の画面が出力されます。$this->Security->blackHollCallBackを設定すれば、任意の画面に差し替えが可能です。
そして、この「デフォルトが空白」という仕様が、Securityコンポーネントの不可解さを助長させている原因のひとつです。debug値に関係なく空白ページが出力されてしまうので、Securityコンポーネントが原因なのかわからず、わかったとしてもどのセキュリティチェックに引っかかったのかまではわかりません。まあ、訓練されたSecurityコンポーネントユーザーであれば、真っ先にパラメータ改ざんチェッカだとあたりをつけますがw
なお、Securityコンポーネントを読み込みつつ、CSRF対策とパラメータ改ざん対策を停止するには、コントローラのbeforeFilter内で$this->Securty->validatePost = false;すればよいです。
CSRF対策は、セッショントークンでフォーム画面からの送信をチェックしている
CSRF対策の基本動作は、セッショントークンによって行われます。
まず、SecurityComponentが読み込まれた時点で、セッションに毎回異なるトークンが保存されます。保存されたトークン値は$form->create()時にhidden値として埋めこまれ、POSTされたときにセッションの値と照合されます。このチェックにより、悪意のあるサイトから勝手にPOSTすることができなくなります。
パラメータ改ざん対策は、フォーム部品情報をハッシュで確認している
パラメータ改ざん対策は、フォーム部品情報のハッシュをhidden値に含めて、POSTデータと整合性をチェックしています。ハッシュ値は$form->end()時にhidden値として埋めこまれます。個人的には初めて見る、興味深い実装です。
このチェックより、悪意のあるユーザーがフォームに存在しないキーを予測して、意図しないDBのフィールドを書き換えるなどの攻撃を防ぐことが出来ます。例えばUser.statusのような、ユーザーが通常変更できないフィールドなどです。
同じような機能として、Model::save()の第三引数にに書き換え可能なフィールドのホワイトリストを渡して書き換えるフィールドを制限することができます。ですが、Securityコンポーネント使用時はそもそもビューで作成していないフィールドのデータが渡ってくることはなくなるので、これは不要となります。より安全にしたいということであれば指定すればよいですが、一つのアクションでビューが複数あるときなどは、ホワイトリストの指定も複数に渡ってしまうので、ビューに任せるほうがコントローラをシンプルに保てるなどの利点もあります。
パラメータ改ざん対策で受付可能なフォーム部品、そうでない部品
では実際に、受付可能なフォーム部品と、不可能な部品の例を見てみましょう。
受け付けるフォーム部品
見慣れたフォーム部品ですね。
echo $form->create('User', array('url' => array('controller' => 'users', 'action' => 'edit'))); echo $form->input('User.name', array('type' => 'text')); echo $form->submit(); echo $form->end();
受け付けないフォーム部品
手書きフォームは全てNGです。こういうフォームが一つでもあると、blackHoleに吸い込まれます。(POSTメソッドに限りますよ)
<form action="/users/edit/<?php echo h($user_id) ?>" method="post"> <input type="text" name="name" value="" /> <input type="submit"> </form>
細かいことですが、formの閉じタグも$form->end()で書かないとダメです。$form->end()でフォーム部品情報のハッシュが出力されているからです。
Formヘルパーを使ったとしても受け付けてくれない書き方
さてここが一番のポイント。なんと、Formヘルパーを使ったとしても受け付けてくれない場合があります。まだハマったことの無い方は覚えておいて損はありません。
name属性の指定は不可
$form->input('User.name', array('type' => 'text', 'name' => 'user_name')); // name属性を指定しているとダメ
第一引数の書き方次第では不可
$form->input('User.genre.' . (int)$i, array('type' => 'checkbox')); // この書き方はダメ
こっちならOK
$form->input('User.' . (int)$i . '.genre', array('type' => 'checkbox')); // OK
数字じゃなければこれもOK
$form->input('User.genre.' . $genre_name, array('type' => 'checkbox')); // $genre_nameがis_int falseであればOK
これは見つけるのに苦労しましたね...。
hidden値の書き換えは、原則不可
これも大いにハマったのですが、type=hiddenなvalue値をJavaScriptなどで書き換えたりすると、通常は受け付けてくれません。hiddenで埋め込んだ値の書き換え防止機能が働いてしまうのです。(hidden書き換え防止自体は便利です)
<?php echo $form->input('User.age', array('type' => 'hidden', 'value' => '') ?> <script type="text/javascript"> function set_user_age(age) { $('#UserAge').val(age); // 書き換えダメ! } </script>
書き換え可能にするには、コントローラのbeforeFilter内でdisabledFieldsをセットしてCSRFチェックの対象から外す必要があります。beforeFilter内で行う理由は、チェックの動作がアクションメソッドの実行前に行われるからです。
例えばUser.ageをチェックから外すには、以下のように書きます。
function beforeFilter() { $this->Security->disabledFields = array('User.age'); }
しかしbeforeFilterはコントローラ一括で動作してしまうので、アクションごとに設定するには場合分けが必要になってしまい、複雑になってしまいます。そこで、微妙さはどっこいどっこいですが、個人的にはdisplay:noneなtextフォームを作ってhiddenの代用としたりしています。
動作の詳細は、今のところソース読めとしか...
これらの動作については、公式ドキュメントには残念ながら記載されていません。Securityコンポーネントを使いこなすには今のところソースを読むのが最も近道という状態です。
色んな意味で難易度の高いSecurityコンポーネントですが、それでもコアヘルパーの一つであり、CSRF対策やパラメータ改ざん対策は使いどころも多いので、気合を入れて使いこなしてみるのもいいと思います。Securityコンポーネントなんか怖くないよ!と言えると、他のCakePHPerからちょっとばかし畏怖の目で見られるかもしれませんw
Advent Calendarは記事を書くいいきっかけ
最後に。Advent Calendar向けに記事を書いてみて思いましたが、こういった締切りのあるイベントへの参加表明で執筆を自己強制するのは悪くないですね。個人的に忙しい時でブログの更新頻度が落ちていたので、いいきっかけになりました。Advent Calendar参加者同士の一体感も感じられるので、楽しいです。来年も何か参加したいですね。
というわけで、CakePHP Advent Calendar 2011の1日目となる方にバトンを放り投げて終わりにしたいと思います。お疲れさまでした!メリークリスマス!
- "CakePHP Advent Calendar 2010を開催!" フォーラム - CakePHP Users in Japan
- CakePHP: 高速開発 php フレームワーク。 Home
- cakephp_jp_advent_cal_2010 (Advent Calendarで書かれた記事のRSS)
[...] 極める!Security Component (CakePHP Advent Calendar 2010 24日目) : akiyan.com TwitterFacebook印刷関連記事 [...]
はじめまして。
2年前の記事にツッコミで恐縮なのですが一部誤りではないかと思ったのでコメントさせて頂きます。
受け付けないフォーム部品の所で「blackHoleに吸い込まれます」とされている例ですが、実際は_validatePost内の
if (empty($controller->data)) {
return true;
}
によりチェックをすり抜けます(なのでCSRFが成立する可能性があります)。
例示されている手書きフォームのような場合$controller->dataが空になるためです。
そもそもヘルパー使ってない時点でまずいよねって話ではあるのですが「セキュリティコンポーネントをオンにして動いてるから安心」とならないパターンもありうるということで一応細かいことながら突っ込ませて頂きました。
なお、動作確認は1.3.13で行いましたが、1.3.6から該当箇所は変更がないので同じように動作すると思われます。