少し設計よりの話を書くとそれに関連する話を書きたくなったので出力の話は後日書きます。
契約による設計(契約プログラミング)(Design by Contract – DbC)は優れた設計・プログラミング手法です。契約による設計と信頼境界線について解説します。
契約プログラミングとは
契約プログラミングは比較的新しい設計思想で、サポートしている言語にはEffile、D、Clojure、Valaなどがあります。最近作られた言語の多くが契約プログラミングをサポートする機能を持っています。C++、C#やJavaなどでも契約プログラミングをサポートするライブラリが利用できます。契約プログラミングをサポートする言語やライブラリを利用しない場合でも、契約プログラミングの概念を適用すると安全かつ効率が良い開発の手助けになります。
Wikipdiaの契約プログラミングの項目では以下のように解説されています。
契約は、コードの利用条件が満たされることによって成立する。
それら条件は、満たすべきタイミングと主体によって、以下の
3種類に分けられる。事前条件 (precondition)
サブルーチンの開始時に、これを呼ぶ側で保証すべき性質。事後条件 (postcondition)
サブルーチンが、終了時に保証すべき性質。不変条件 (invariant)
クラスなどのオブジェクトがその外部に公開しているすべ
ての操作の開始時と終了時に保証されるべき、オブジェク
ト毎に共通した性質。コードを呼ぶ側が事前条件と不変条件を満たす義務を負うこ
とで、呼ばれたコードはその条件が恒真であるとの前提を利
益として得る。引き換えに、呼ばれたコードは事後条件と不
変条件を義務として負い、呼ぶ側の利益としてこれを保証す
る。
サブルーチン(つまり関数、メソッドコール)の開始時に呼び出す側が必要とする条件をすべて満たし、呼び出された側は終了時に特定の条件で終了するように設計・プログラミングする手法です。
擬似コードで書くと次のような書き方になります。
require PRE_CONDITION // 事前条件 と 不変条件 function() { // 処理 } ensure POST_CONDITION // 事後条件 と 不変条件
これがなぜ良い設計・プログラミング手法であるかは明白です。呼び出す側で、呼び出される側が必要とする条件を満たす事を保証するので、呼び出される側は条件が満たされているかチェックする必要がありません。一般に呼び出し階層が深くなればなるほど繰り返し実行される可能性が高くなります。事前条件が上位の階層でチェックされていれば下位の階層でのチェックが必要なくなり効率的なプログラムなります。
契約プログラミングの有用性が分かる事例
解りやすい例は文字エンコーディングのバリデーションです。文字エンコーディングが妥当であるとバリデーションされている事が前提条件であれば、呼び出られた側は文字エンコーディングのバリデーションを行う必要はありません。つまり、処理をより高速かつ安全に実行できる事を意味します。
PHPのmbstringモジュールやiconvモジュールなどのマルチバイト文字を取り扱う機能は文字エンコーディングのバリデーションを行っています。しかし、PHPプログラマが契約による設計を行っていれば文字エンコーディングのバリデーションを何度も何度も繰り返す必要はありません。呼び出す側が文字エンコーディングは妥当であるとする契約を守るのであれば、不必要な文字エンコーディングバリデーションを行わずに済みます。文字エンコーディングのバリデーションは必須ですが、同じアプリケーション上で同じバリデーションコードを使って同じ文字列を繰り返し何度も文字エンコーディングの妥当性チェックを行うのは無駄でしかありません。
参考: 文字エンコーディングをバリデーションしないとDoSに脆弱になる
契約プログラミングの概念がないと非効率なコードを生産する
キャンセルされたUnicode対応のPHP6が失敗した理由はまさにこれが理由でした。プログラマが明示的に文字エンコーディングをチェックせず「呼び出された側」つまりPHPが自動的に文字エンコーディングを確認し適切に処理するようにしようとした為、内部処理が複雑化し大幅にパフォーマンスが低下する原因になりました。一部には「文字エンコーディングの整合性はインフラ(つまり言語/ライブラリ/フレームワーク)が自動的に適切であることを保証すべき」とする考えもあるようですが、これは現実的ではないことが失敗したPHP6から学べる事実です。Unicode対応を目指したPHP6の開発に契約プログラミングの概念があれば失敗する事は無かったでしょう。
(フレームワークであれば、入力バリデーションの仕組みを利用しプログラマにあまり意識させる事無く文字エンコーディングのバリデーションを行う事は可能です。しかし、テキストもバイナリも同様に受け入れるPHPの場合、言語レベルでは実用的ではありません。)
PHP内部での文字エンコーディング処理を例に挙げましたが、契約によるプログラミングが一般のアプリケーションプログラミングにおいても有用である事は明らかです。例えば、呼び出される関数の引数に数値が必要とされる場合に呼び出し側でチェックしていれば、呼び出される側でチェックする必要はありません。
前提条件を呼び出す側がチェックする例
// 前提条件のチェック if (!is_numeric($val)) { trigger_error(E_USER_ERROR, 'Should not happen'); } // 10万回計算を繰り返して結果を求める for ($i = 0; $i < 100000; $i++) { $val = useful_compute($val); } function useful_compute($val) { // 何らかの有用な計算 return $val; }
前提条件を呼び出される側がチェックする例
// 10万回計算を繰り返して結果を求める for ($i = 0; $i < 100000; $i++) { $val = useful_compute($val); } function useful_compute($val) { // 事前条件のチェック if (!is_numeric($val)) { trigger_error(E_USER_ERROR, 'Should not happen'); } // 何らかの有用な計算 return $val; }
後者は前者に比べ、不必要な値のチェックを10万回も多く行う事になります。前提条件のチェックを呼び出される側に押し付けると、同じ条件チェック、つまり本来は省略できるチェックを繰り返し行う事になります。前提条件チェックを呼び出される側で行うとプログラムの構造上、多かれ少なかれそうなってしまいます。効率が良く、信頼性の高いアプリケーションを開発するには、前提条件のチェックはできるだけ早い段階で行うべきなのです。
現在のPHPで契約による設計を利用する
PHP本体に契約による設計のサポートも提案準備中ですが、今のところ導入されるかどうかは分かりません。
現在のPHPでもassert()を使って契約による設計を行えます。先程の簡単なサンプル関数を契約による設計に変えてみます。
// 10万回計算を繰り返して結果を求める for ($i = 0; $i < 100000; $i++) { $val = useful_compute($val); } function useful_compute($val) { // 事前条件のチェック assert(is_numeric($val)); // 何らかの有用な計算 return $val; }
単純にif文で行っていた条件チェックを、assert()に置き換えるだけです。
PHP7からassert()を無効化した場合のオーバーヘッドはゼロになりました。無効にするとassert()がない状態を同じ性能でコードを実行できます。
シンプルなコードで事前条件しかありませんが、同じ要領で事後条件と不変条件もチェック可能です。
作ったソフトウェアを運用する場合はassert()は無効化します。assert()は実行されないので、useful_compute()が正しく動作する為の事前条件の保証は”呼び出す側”で保証しなければなりません。
このように契約による設計を使うと、呼び出す側はエラーが発生しないような妥当なデータを渡す責任を持ちます。開発時には実行速度が遅くなりますが、コードが正しく動作する為の条件チェックが関数/メソッドで実行され、おかしな変数が渡された場合にassertionエラーとなり即座にバグを検出可能となります。
契約による設計を使ったプログラムは、assert()によるデータ/状態の妥当性チェックだらけになります。このため開発時の速度がかなり遅くなることもありません。しかし、何の問題もありません。
運用時にはassert()の実行は全て省略されます。assert()を実行しない為、より高速に実行できます。上位(呼び出す側)の関数/メソッドは不正な値が渡されないように保証しないとプログラムは正常に動作しなくなります。
不正な値がアプリケーション内の関数/メソッドに渡されないようにするには、少なくとも、アプリケーションの入り口(=信頼境界)で、入力値をバリデーションしなければなりません。アプリケーションの入り口(=信頼境界)には「上位(呼び出す側)の関数/メソッド」が無いので、必ず入力バリデーションが必要になります。
信頼境界はアプリケーションの中にある場合もあります。例えば、どのように実装されているのかよく分らないライブラリ、頻繁に更新されるライブラリの場合、そのライブラリを利用する時(ライブラリ関数を呼び出す時=ライブラリへの出力時)にパラメーターのチェックをアプリ側で行います。このライブラリはアプリケーションにとって信頼境界の外にあることになります。
契約による設計と信頼境界線
アプリケーションプログラミングにおいて契約による設計(契約プログラミング)を行う場合、信頼境界線を明確に意識する事が必須です。信頼境界線とはプログラムが入力されるデータやプログラムを信用してよいかどうか?を基準に境界線を引きます。
アプリケーションにおいて信頼できる入力データとは「プログラムにハードコードされたデータ、安全性が保証されているデータのみ」です。他のデータは全て信頼できないデータです。
例えば、JavaScriptでデータ形式をバリデーションしたブラウザからのフォームデータ入力は信頼できません。フォームデータのみで無く、ブラウザから送信されたデータ全てが信頼できません。送信者が確実に「契約」(つまり約束事)を守るとは限らず、データを送信してきた送信者は攻撃用のプログラムかも知れないからです。ファイルやデータベースに保存されたデータも、信頼できると保証できなければ信頼できないデータです。ファイルやデータデータには攻撃者が保存したデータが含まれる可能性があるからです。
アプリケーションの入出力処理と契約による設計
図:アプリケーションのセキュリティ設計の基本
アプリケーションに於ける入力バリデーションと出力の安全性を説明した図です。契約プログラミングは全ての粒度で入力/出力(+状態)のバリデーションします。
契約による設計をアプリケーションプログラミングに適用するにはどうすべきでしょうか?もう既に答えは出ています。呼び出し側が契約(約束事)を守らない可能性があるなら、呼び出された側が契約どおりであるかチェックします。
どこでチェックすべきでしょうか?これも答えは既に出ています。呼び出された側は入力処理の最初にチェックをすべきです。つまり、MVCアーキテクチャのフレームワークであればコントローラーが入力を受け付けた直後にチェックすべきです。前提条件の確認は出来る限り早い段階で行う事が契約による設計のポイントです。モデルで更に確認が必要になる場合もありますが、コントローラーで出来ることはコントローラーで処理してしまう方が良いです。
そもそもモデルは入力パラメーターをチェックするには適さない場所です。呼び出し側であるコントローラーで確認できる事を呼び出される側のモデルで確認する設計は契約による設計ではありません。特にデータのバリデーションがデータ保存時に行われるよフレームワークを利用している場合にはコントローラーでのバリデーションの重要性が増します。データがバリデーションされる前に利用される可能性があるからです。
コントローラーとモデルが一対一の関係になる単純なアプリケーションであれば、モデルにチェックを押し付けても上手く行く事も多くあります。しかし、コントローラーとモデルが一対一の関係にあるような単純なアプリケーションでもモデルにチェックを押し付けるメリット(チェックの手間をモデルに集約するメリット)はありません。本来は入力処理でチェックすべき処理を遅らせる事により、リスクを増加させているに過ぎません。
入力と同様にアプリケーションが出力を行う場合にも「契約」を確実に実行しなければなりません。つまり、出力先のシステムが予期する入力、誤作動しない入力となるような出力を行わなければなりません。モデルにはデータベースやファイルなどへの出力が契約に合っているかチェックする役割があります。特に、データベースに参照整合性を任せていないシステムの場合は、より厳重に出力のチェックを行う必要があります。
出力時のチェックは外部からの入力が契約に合っているか?チェックすることとは別のチェックです。契約を履行する責任は呼び出し側にあります。つまり、出力する側が正しい出力を行うことが求められます。入力のチェックと重複が多くなる場合もありますが、信頼境界線を超える出力は出力が契約通りであるか保証しなければなりません。
契約プログラミングをサポートする言語では契約のチェックはプロダクションコードではチェックされないようにできます。つまりアプリケーションへの入力確実に検証し、アプリケーションからの出力は出力先に適合するようにプログラムが書かなければなりません。安全性が高く、効率が良いプログラミングはアプリケーションの入出力を確実に行う事で達成できます。
Webアプリケーションは基本的にリクエストに対してレスポンスを返すだけの単純なプログラムです。このためWebアプリケーションと契約プログラミングの相性は非常良く、異常が発生した場合は単純にエラーとして処理を中止できます。
注:入力ミスなどの処理は正常系(準正常系)の処理です。
契約による設計は言語を問わない
PHPにはassert関数はありますが、契約プログラミングをサポートする機能を持っていません。しかし、契約プログラミングの概念は十分実用的に利用できます。セキュリティ対策の多くはパフォーマンスとセキュリティのトレードオフ関係にある事も多いです。しかし、無闇に無用なパフォーマンスペナルティを与える必要はありません。契約プログラミングの概念を取り入れることにより、安全かつ実行効率の良いプログラミングが行えるようになります。
信頼境界線の書き方
単独で動作するアプリケーションの場合、信頼境界線を入出力で引くことが必須です。アプリケーションの中でも、自分(自分たち)が書いたコードと他人が書いたコードで境界線を引いたり、モジュール単位などで境界線を引くと良いです。特に大きなプロジェクトの場合はアプリケーション内でも境界線を引いた方がセキュリティ的には良いです。信頼境界線をアプリケーション内に引くと、チェックの為のオーバーヘッドが必要になりますが、セキュリティとパフォーマンスはトレードオフ関係です。適切なレベルで調整すれば問題ないと考えます。
アンチパターンとして文字エンコーディングのバリデーションを挙げましたが、このような低いレベルのチェックをライブラリ等の中に押しこむことはセキュリティ的にも意味が無い上、パフォーマンスとのトレードオフが大きいです。同じデータに対して、同じコードで、何度もチェックを繰り返す事になるからです。セキュリティ的に意味がない理由は文字エンコーディングのチェックをしていないライブラリなどが1つ混ざっているだけで、アプリケーションの動作が保証できなくなります。
文字エンコーディングが妥当であるかチェックするのはアプリケーションレベルでの入力処理で行い、アプリケーションからの出力時にもう一度チェックするのが妥当な設計です。
PHPの場合、mb_check_encoding関数で全てのテキスト入力のエンコーディングはバリデーションします。
htmlentities/specialcharsはエンコーディングのバリデーションを行います。サンプルとして書いたescape_javascript_string関数はmbstring関数でUnicodeの文字コードに変換しているので文字エンコーディングのバリデーションは行われています。ただし、出力時のバリデーションは「フェイルセーフ」(おかしなデータが渡された時に安全に失敗させる機能)なので、入力処理でバリデーションしておかないとDoSに脆弱になり得ます。
SANS/CWE TOP25でも入力と出力の確実な制御は最大のセキュリティ対策として挙げています。今時の設計手法である契約による設計を行う場合にも入力と出力を確実に制御することが重要です。契約による設計が優れた概念であることは、新しく開発された言語の多くが実装していることからも明らかです。構造化設計に慣れている方の場合、アプリケーションの奥深くにデータや条件など妥当性チェックを埋め込んでしまう傾向にあります。この考え方は既に古い考え方であると捉えるべきでしょう。
残念ながら契約による設計の概念に対応しているWebアプリケーションフレームワークが普及しているとは言い難いです。しかし、対応していなくてもアプリケーションの入出力を確実に制御することは可能です。工夫しながら対応すると良いでしょう。
まとめ
セキュアなソフトウェアアーキテクチャー(データに着目したセキュアな構造)は以下のようになります。
契約プログラミングをサポートする言語では、「契約」(つまり入力/出力/状態)のチェックは開発時にしか行いません。これでは危険なので、適当な箇所に「信頼境界線」を引き多層防御します。アプリケーションとライブラリでは「役割と責任」が異なります。アプリケーションレベルの入力バリデーション/出力の安全化は必須であることを覚えておきましょう。
適切な契約プログラミングにはゼロトラストとフェイルファーストが欠かせません。その上でセキュリティ対策を構造化すると上記の図のような構造になります。
PHPのセキュリティ入門書に記載するコンテンツのレビューも兼ねてPHP Securityカテゴリでブログを書いています。コメント、感想は大歓迎です。
参考:
Leave a Comment