防御的プログラミングは、潜在的なセキュリティ異常を検出し、あらかじめ定められた対応を行うプログラムを開発することを目的とした防御設計の一形態です。 [ 1 ]予期せぬ状況下でもソフトウェアの継続的な機能を保証します。防御的プログラミング手法は、高可用性、安全性、またはセキュリティが求められる場面でよく用いられます。
防御的プログラミングとは、ソフトウェアとソースコードを改善するためのアプローチであり、以下の点において有効である。
しかし、過度に防御的なプログラミングは、決して発生しないエラーを防ぐことになり、結果として実行時間とメンテナンスのコストが増加する可能性がある。
セキュアプログラミングは、コンピュータセキュリティに関心を持つ防御プログラミングのサブセットです。セキュリティが重視され、必ずしも安全性や可用性が重視されるわけではありません(ソフトウェアが特定の形で障害を起こすことが許容される場合もあります)。あらゆる種類の防御プログラミングと同様に、バグを回避することが主要な目的ですが、その動機は(安全性が重視される場合のように)通常動作時の障害発生確率を低減することではなく、攻撃対象領域を縮小することにあります。プログラマーは、ソフトウェアがバグを明らかにするために積極的に悪用される可能性があり、またバグが悪意を持って悪用される可能性があることを前提としなければなりません。
int risky_programming ( char * input ) { char str [ 1000 ]; // ... strcpy ( str , input ); // 入力をコピーします。// ... }入力が1000文字を超えると、この関数は未定義の動作を引き起こします。プログラマーの中には、ユーザーがこれほど長い入力を入力することはないだろうと考えて、これを問題視しない人もいるかもしれません。しかし、このバグはバッファオーバーフローの悪用を可能にする脆弱性を示しています。以下に、この例に対する解決策を示します。
int secure_programming ( char * input ) { char str [ 1000 + 1 ]; // ヌル文字用にもう1つ。// ...// 入力をコピー先の長さを超えないようにコピーします。strncpy ( str , input , sizeof ( str ));// strlen(input) >= sizeof(str) の場合、strncpy はヌル終端しません。// バッファの最後の文字を常に NUL に設定することで、// 文字列を処理可能な最大長に切り詰めることで、この問題を解決します。 // また、strlen(input) が長すぎる場合は、プログラムを明示的に中止することもできます。 str [ sizeof ( str ) - 1 ] = '\0' ;// ... }攻撃型プログラミングは防御型プログラミングの一種ですが、特定のエラーは防御的に処理すべきではないという点が強調されています。この手法では、プログラムの制御範囲外のエラー(ユーザー入力など)のみを処理し、ソフトウェア自体、およびプログラムの防御ライン内部のデータについては信頼できるものとみなします。
const char * trafficlight_colorname ( enum traffic_light_color c ) { switch ( c ) { case TRAFFICLIGHT_RED : return "red" ; case TRAFFICLIGHT_YELLOW : return "yellow" ; case TRAFFICLIGHT_GREEN : return "green" ; } return "black" ; // 信号機が停止している場合に処理します。}const char * trafficlight_colorname ( enum traffic_light_color c ) { switch ( c ) { case TRAFFICLIGHT_RED : return "red" ; case TRAFFICLIGHT_YELLOW : return "yellow" ; case TRAFFICLIGHT_GREEN : return "green" ; } assert ( 0 ); // このセクションに到達できないことをアサートします。}if ( is_legacy_compatible ( user_config )) { // 戦略: 新しいコードが古いコードと同じ動作をするとは限らないことを考慮に入れる。old_code ( user_config ); } else { // フォールバック: 新しいコードが同じケースを処理するとは限らないことを考慮に入れる。if ( new_code ( user_config ) != OK ) { old_code ( user_config ); } }// 新しいコードに新しいバグがないことを確認するif ( new_code ( user_config ) != OK ) { // 適切な注意を引くために、エラーを大声で報告し、プログラムを強制終了するreport_error ( "何かがひどく間違っていました" ); exit ( -1 ); }以下に、防御的なプログラミング手法をいくつか紹介します。
既存のコードがテスト済みで正常に動作することが確認されている場合、それを再利用することで、バグが混入する可能性を低減できる。
しかし、コードの再利用は必ずしも良い方法とは限りません。既存のコードを再利用すると、特に広く配布されている場合は、通常では不可能なほど広範囲のユーザーを標的とした攻撃が可能になり、再利用されたコードが持つセキュリティ上のリスクや脆弱性もすべて引き継ぐことになります。
既存のソースコードの使用を検討する際には、モジュール(クラスや関数などのサブセクション)を簡単に確認することで、潜在的な脆弱性を排除したり、開発者が脆弱性を認識したりすることができ、プロジェクトでの使用に適しているかどうかを確認できます。
古いソースコード、ライブラリ、API、設定などを再利用する前に、それらが再利用に適しているか、あるいはレガシーシステム特有の問題が発生する可能性が高いかどうかを検討する必要があります。
レガシー問題とは、古い設計が今日の要件を満たすことを期待する際に必然的に生じる問題であり、特に古い設計がそれらの要件を念頭に置いて開発またはテストされていない場合に顕著になります。
多くのソフトウェア製品は、古いレガシーソースコードに起因する問題を抱えています。例えば、以下のようなものです。
レガシー問題の顕著な例:
悪意のあるユーザーは、誤ったデータの新たな表現方法を考案する可能性が高い。例えば、プログラムがファイル「/etc/ passwd」へのアクセスを拒否しようとした場合、クラッカーは「/etc/./passwd」のような別のファイル名のバリエーションを渡す可能性がある。正規化ライブラリを使用することで、非正規な入力によるバグを回避できる。
問題を起こしやすいと思われるコード構造(既知の脆弱性などと同様)は、バグであり潜在的なセキュリティ上の欠陥であるとみなしてください。基本的な原則は、「あらゆる種類のセキュリティ攻撃を把握しているわけではない。自分が認識している攻撃に対しては対策を講じ、さらに先を見越した対策を講じなければならない」ということです。
getsscanfデータセキュリティに関する以下の3つのルールは、社内または社外から取得したあらゆるデータの取り扱い方法を定めたものです。
すべてのデータは、そうでないと証明されるまでは重要である。つまり、すべてのデータは破棄される前に、ゴミデータとして検証されなければならない。
すべてのデータは、そうでないと証明されるまでは汚染されているとみなされる 。つまり、すべてのデータは、整合性を検証せずにランタイム環境の残りの部分を危険にさらすような方法で処理されなければならない。
コードは、そうでないと証明されるまでは安全で はない。これはやや不適切な表現ではあるが、バグや未定義の動作によってプロジェクトやシステムが一般的なSQLインジェクション攻撃などの攻撃にさらされる可能性があるため、コードが安全であると決して思い込まないようにという良い教訓を与えてくれる。