プログラム・コードが正しく動作する為の必要条件と十分条件を考えたことがあるでしょうか?
プログラム・コードが正しく動作する為の必要条件と十分条件とは何でしょうか?
サンプルコード
簡単な整数足し算を行うコード(add.php)です。
<?php // ファイル名: add.php // 整数(符号付き64bit整数)の足し算を行うプログラム // 使用例: // php add.php 123 456 // 出力: // 579 // 足し算関数 function add($x, $y) { return $x + $y; } // メインプログラム echo add($argv[1], $argv[2]);
コメントから、2つの符号付き64ビット整数を引数として加算して結果を出力するプログラムだと分かります。
このコードは正しく動くでしょうか?
実際に動かしてみます。
$ php add.php 123 456 579
正しく動作しました。
しかし、本当にこのコードは正しく動作するでしょうか?
サンプルコードが正しく動作する為の条件
サンプルコードは”2つの符号付き64ビット整数を引数として加算して結果を出力するプログラム”ですが正しく動作しません。正確には、常に正しく動作しません。
プログラミング経験者ならサンプルコードには色々な問題があることが分かると思います。
- 64bit整数を取り扱える環境か確認していない
- 入力パラメータ($argv[1], $argv[2])の存在を確認していない
- 入力パラメータデータが整数($argv[1], $argv[2], $x, $y)であるか確認していない
- 入力パラメータのデータが整数(符号付き64bit整数)として演算可能な値($argv[1], $argv[2], $x, $y)であるか確認していない
- 演算が整数(符号付き64bit整数)として表現可能な範囲内での結果であったか確認していない
これらの問題点から、このプログラムは正しく動作するための条件を満たしていないことが分かります。
正常動作に必要な条件と十分となる条件に分けて考えます。
必要十分条件の定義
「pならばq」の命題が真のとき、qはpの必要条件、pはqの十分条件である
このadd.phpプログラムの場合の次のようになります。
命題と必要十分条件
「add.phpの実行結果が正しいならばadd.phpのパラメータは正しい」、パラメータの正しさは必要条件、実行結果の正しさは十分条件である
言い換えると「add.phpの実行結果が正しいことを保証する為には、”入力値として正しい”パラメーターが必要」ということです。
コンピューターは一定の条件下でしか正しく動作しません。入力値が適切なデータ型であることは”最低限の条件”でしかありません。整数の足し算が正しく動作する為の条件を考えてみます。
正常動作の必要条件
add.phpが正しく動作する為の必要条件、つまりパラメータが正しいことを満す条件、は以下の通りです。
- 64bit整数を取り扱える環境か確認していない
- 入力パラメータ($argv[1], $argv[2])の存在を確認する
- 入力パラメータデータが整数($argv[1], $argv[2], $x, $y)であるか確認する
- 入力パラメータのデータが整数として演算可能な値($argv[1], $argv[2], $x, $y)であるか確認する
最初の条件(64bit整数を取り扱える環境か確認していない)以外は、セキュアコーディングの「外部入力のバリデーション」です。契約プログラミングでは「事前条件(precondition)」です。
必要条件チェックの追加
add.phpの正常動作の必要条件を追加後
<?php // ファイル名: add.php // 整数(符号付き64bit整数)の足し算を行うプログラム // 使用例: // php add.php 123 456 // 出力: // 579 // 足し算関数 function add($x, $y) { return $x + $y; } ////// 正常動作の必要条件 /////// // 64bit整数サポートのチェック if (PHP_INT_SIZE != 8) { die('64bit整数をサポートしていません'); } // 第一パラメータの必要条件チェック if (!isset($argv[1])) { die('第一引数が与えられていません'); } if (strspn($argv[1], '1234567890') !== strlen($argv[1])) { die('第一引数の値が整数形式ではありません'); } if (bccomp(PHP_INT_MAX, $argv[1]) === -1) { die('第一引数の値が符号付き64bit整数の上限以上です'); } if (bccomp($argv[1], PHP_INT_MIN) === -1) { die('第一引数の値が符号付き64bit整数の下限以下です'); } // 第二パラメータの必要条件チェック if (!isset($argv[2])) { die('第二引数が与えられていません'); } if (strspn($argv[2], '1234567890') !== strlen($argv[2])) { die('第二引数の値が整数形式ではありません'); } if (bccomp(PHP_INT_MAX, $argv[2]) === -1) { die('第二引数の値が符号付き64bit整数の上限以上です'); } if (bccomp($argv[2], PHP_INT_MIN) === -1) { die('第二引数の値が符号付き64bit整数の下限以下です'); } // メインプログラム echo add($argv[1], $argv[2]);
正常動作の十分条件
add.phpが正しく動作する為の十分条件、つまり実行結果が正しいことを満す条件、は以下の通りです。
- add関数の演算が整数として表現可能な範囲内での結果であったか確認する
この十分条件チェックは、セキュアコーディングの「出力の無害化」、契約プログラミングでは「事後条件(postcondition)」です。
条件を満たさなければコンピューターは数値演算を正しく行なえません。add関数へのパラメータが十分に小さい場合[^overflow]、この十分条件は自動的に満たされますが、パラメータの範囲に条件がないので足し算により整数オーバーフローが発生する場合があります。
十分条件チェックの追加
<?php // ファイル名: add.php // 整数(符号付き64bit整数)の足し算を行うプログラム // 使用例: // php add.php 123 456 // 出力: // 579 // 足し算関数 function add($x, $y) { ////// 正常動作の十分条件 ////// // 任意精度整数演算を使い、オーバーフローをチェック $tmp = bcadd($x, $y); if (bccomp($tmp, PHP_INT_MIN) === -1 || bccomp(PHP_INT_MAX, $tmp) === -1) { die('演算結果が大き過ぎました'); } return $tmp; } ////// 正常動作の必要条件 /////// // 64bit整数サポートのチェック if (PHP_INT_SIZE != 8) { die('64bit整数をサポートしていません'); } // 第一パラメータの必要条件チェック if (!isset($argv[1])) { die('第一引数が与えられていません'); } if (strspn($argv[1], '1234567890') !== strlen($argv[1])) { die('第一引数の値が整数形式ではありません'); } if (bccomp(PHP_INT_MAX, $argv[1]) === -1) { die('第一引数の値が符号付き64bit整数の上限以上です'); } if (bccomp($argv[1], PHP_INT_MIN) === -1) { die('第一引数の値が符号付き64bit整数の下限以下です'); } // 第二パラメータの必要条件チェック if (!isset($argv[2])) { die('第二引数が与えられていません'); } if (strspn($argv[2], '1234567890') !== strlen($argv[2])) { die('第二引数の値が整数形式ではありません'); } if (bccomp(PHP_INT_MAX, $argv[2]) === -1) { die('第二引数の値が符号付き64bit整数の上限以上です'); } if (bccomp($argv[2], PHP_INT_MIN) === -1) { die('第二引数の値が符号付き64bit整数の下限以下です'); } // メインプログラム echo add($argv[1], $argv[2]);
必要条件と十分条件
世の中のプログラム/コードには、必要条件を満たさず(確認しないで)、十分条件だけ満すような体裁になっている物が多くあります。
必要条件の確認があっても、その確認を出来るだけ深いレベルの関数/メソッドでチェックする体裁のプログラム/コードになっている場合も多くあります。
一見すると上記のような体裁でも構わないように思うかも知れません。しかし、上記のような設計は複雑性とセキュリティ上のリスクも増加させます。更に悪いことに、実行時の性能も悪化させます。
- 十分条件の確認だけでは、プログラムが正しく動作していることを保証できない。
'abcde' + 1234 = 1234
といった演算が正しくないことは一目瞭然です。’abcd’を数値として保存した場合のセキュリティリスクも含む影響を正確に評価するのは、大きなプログラムだとかなり大変1です。参考: SQLite3のカラムは全て文字列
- 必要条件の確認は出来るだけ早い段階で確認しないと、不必要な必要条件の確認を繰り返し行わなければならない
例)PCRE(Perl互換正規表現ライブラリ)はUTF-8文字列マッチの際に、文字エンコーディングバリデーション(正常動作の必要条件)を省略することができます。文字エンコーディングバリデーションを省略すると、場合によっては数十倍の高速化が可能です。
更に
- プログラムが正常に動作させる為の必要条件を分散させると、漏れが発生しても気づけない
といったリスクが発生します。「漏れがあっても気づけない」はセキュリティ問題の原因の1つで、かなりの割合でこれが原因で脆弱になっています。
アプリケーションレベルでは「プログラムが正常動作する必要条件の確認」は実施して当たり前と言えます。しかし、多くのアプリケーションでは当たり前(入力パラメーターのバリデーション)が実施されていません。実施していても正常動作を保証するバリデーションになっていないことが多いです。
ライブラリレベルでは「プログラムが正常動作する必要条件の確認」は実施して当たり前ではありません。PCREの例の様に、確認すると何十倍、場合によっては何百倍も遅くなることがあります。
アプリケーションレベルでの必要条件の確認は必須です。このためセキュアコーディング一番目のセキュリティ対策として入力バリデーションが挙げられています。論理的に考えると当然の結論ですが、残念ながら浸透しているとは言い難いのが現状です。
まとめ
プログラムを正しく(かつ効率的に)動作させる為の構造を論理的に考えると
を実施するとセキュリティ的にも、実行時の性能的にも、コードのメンテナンス性も2向上します。
良い事ずくめです!
しかし、残念ながら適切な構造と習慣を採用していないケースが非常に多いです。効率的な構造と習慣は難しくありません。効率的なプログラム/コードを書きましょう!