セキュアプログラミング(防御的プログラミング)の歴史をざっと振り返る

(更新日: 2016/07/22)

セキュアプログラミング(防御的プログラミング)の歴史をざっと振り返ってみたいと思います。セキュアプログラミングは防御的プログラミングとも言われるプログラミングの原則の1つ※です。古くからある概念ですが、誤解または理解されていない概念の1つではないでしょうか?

Defensive Programmingとして記載されています。

何故、一般に広く常識として理解されていないのか?その理由は防御的プログラミングの歴史にあるのかも知れません。

参考:セキュアプログラミングの7つ習慣

 

 

セキュアプログラミングの必要性が認識された事件

コンピュータセキュリティの基礎的概念は60年代から研究されていました。その成果も踏まえ、インターネットの前身であるARPANETは1969年から稼働を開始しました。現代でも通用する概念が考案されています。しかし、セキュリティを無視していたのではありませんが、ざっくり言うと60年代、70年代、80年代は「プログラムを作る」ことに専念し「セキュアなプログラムを作る」ことはあまり重視されていませんでした。

昔のコンピュータは今と比べるとCPU、メモリ、ストレージ何をとっても遥かに非力で、しかも広域ネットワークはありませんでした。PCはスタンドアローンでシングルタスク、当然メモリプロテクションなど全くなくこの環境で動くプログラムに今必要とされている「セキュリティ」は全く必要あませんでした。必要無かったというよりは、カーネルモード無し/シングルタスク/メモリプロテクション無しのコンピュータでは実現不可能でした。ワークステーションと呼ばれていたUNIX環境などではカーネルモードとユーザーモード、マルチプロセスのプロセス保護、メモリプロテクションはありました。しかし、コンピュータが広域なネットワークに接続されることは今のように一般的ではありませんでした。メインフレームと呼ばれる大型コンピュータは専用のネットワークを利用していました。

つまり、昔は「外部からの攻撃」など考える必要がほとんどなく「予定されているデータ」で「プログラムが動きさえすればOK」だったのです。非力なCPU、小さく遅いメモリ/ストレージ、細いネットワークなどのハードウェア的な制約もあり、セキュリティよりもコンパクトで実行効率が良いプログラムの作成が優先されていました。このような状況であった為、この頃のプログラミング原則には現在は無効になっている物もあります。(同じ処理は一回限り、は無効です。セキュアなプログラムでは縦深防御/多層防御を実装します)

バッファーオーバーフローを利用する攻撃は少なくとも1972年から認知されていました。 攻撃の可能性は指摘されても、実際に被害がないとなかなか対策が進まないものです。事実、1972年から1988年までプログラムをバッファーオーバーフロー攻撃から防御するという考えはほとんどありませんでした。

これを変える切っ掛けとなった事件が、史上初のインターネットワームと言われる1988年のMorris Worm(モリスワーム)です。この頃既にインターネットが構築され、複数の大学や研究機関のコンピュータ(UNIXワークステーション)がインターネットに接続されていました。当時のUNIXはプロセス分離/メモリ保護/カーネル&ユーザーモードなど、マルチユーザーに必要なセキュリティ機能はありましたが、単純なスタックオーバーフロー攻撃を防御する機能さえありませんでした。モリスワームはrsh、sendmailも使っていますが、fingerdのスタックオーバーフローを利用していました。詳細を知りたい方はモリスワームの分析をした論文を参照してください。このメモリを破壊し、任意コードを実行する攻撃の衝撃は大きく、セキュリティに対する考え方が大きく変ることになります。

この頃のアプリケーション/ソフトウェアは非常に多くのオーバーフロー脆弱性を抱えていました。防御する、という考え方が無かったので当然です。OSも、今のOSからすると考えられないほど脆弱な時代でした。次ぎ次ぎに似たような問題が発生すると考えられました。セキュリティ脆弱性の修正と公開をコーディネートするCERTが作られたのはモリスワーム事件(1988/11/2)があった直後(1988/11/30)でした。

 

攻撃技術の発達と防御

モリスワーム以降、単にネットワークやプロセスを分離したりするだけでなく、プログラム自身のセキュリティ対策が重要であることが広く認識されました。OSのみの防御では不十分であり、ネットワーク(ネットワークファイアーウォールは80年代後半から利用されている。それ以前はルーターによるフィルタリング )、アプリケーションの防御が不可欠である、と理解され防御的なプログラミングが始まりました。

攻撃する側はコンピュータをリモートから乗っ取れるメモリインジェクション攻撃を次々に進化させます。最初は単純なスタックオーバーフローでしたが、整数オーバーフロー、ヒープ領域オーバーフローを利用した攻撃が考案されます。インジェクション攻撃はメモリのみに留まりません。コンピュータが意味を持つ情報を保存/利用している物、全てに波及していきます。

起源を特定するソースが見つかりませんでしたが、ヌル文字インジェクション、メールヘッダーインジェクション、XMLインジェクションなどのインジェクション攻撃の派生型はこの頃考案されたと思われます。

1993年に初めてのDEF CONが開かれます。DEF CONはコンピュータを攻撃するテクニックの競技+セキュリティカンファレンスで、現在も開催されています。初回からの資料はWebサイトで公開されています。少し遅れること1997年からBlack Hatカンファレンスが開かれます。Black HatもDEF CONと同様に毎回注目を集めています。こちらも初回からの資料がWebサイトで公開されています。DEF CON/Black Hatなど、セキュリティカンファレンスは攻撃技術の発達のみでなく、防御する側の発展にも大きく貢献しています。

開発者も何もしなかった訳ではありません。モリスワーム事件から「動くプログラムを作る」から「安全に動くプログラムを作る」方向に変わってきました。私が所有しているCode Complete (Microsoft Press 英語版)には5.6 Defensive Programming(防御的プログラミング)のセクションがあります。たまたまですが、DEF CONと同じ1993年に発行されています。

防御的プログラミングがセキュアなプログラミングに欠かせない、と認識されたのは1993年より以前のはずです。出典が見つけられませんでしたがモリスワームの翌年(モリスワーム事件は1988年11月2日)くらいには、概念が考えられたのではないか?と思われます。

上記の簡易年表を見てわかるように、モリスワーム以降10年ほど新しい攻撃手法が考案されませんでした。これはスタックオーバーフロー脆弱性に対する攻撃のみで攻撃できてしまうプログラムが多数あり、新たな攻撃手法を考案する必要がなかった為ではないかと考えられます。

この間に防御的プログラミングはC言語のようにメモリを直接操作できる言語にのみ必要なセキュリティ対策である、との誤った認識が広まった可能性があります。確かに90年代の防御的プログラミングはメモリ破壊攻撃に対する防御手法として論じられることが多かったと思います。これが防御的プログラミングはC言語のプログラミング手法だと誤認され、あまり一般に広がらなかったことが原因かも知れません。

しかし、防御的プログラミングの基本概念である「全ての外部入力は信用できない」はメモリ破壊攻撃に限る、とは言われていなかったと記憶しています。その様な本/文書があってもおかしくないですが、外部入力は信用できない、確実に安全な出力を行う、という基本概念は変わりません。賢明なコンピューターサイエンティストが、バッファーオーバーフローはプログラムをクラッシュさせるだけ、との間違った認識の反省から「全ての外部入力」に制限を付けて考えていたとは思えません。

年表からは比較的最近も新なインジェクション攻撃が考案されていることが分かります。そしてスタックオーバーフローと同様に、脆弱性のアドバイザリから15年経過した現在でもJavaScriptインジェクション脆弱性はWebアプリのトップに位置する脆弱性のままです。JavaScriptインジェクションは構造的に対処しずらい、Web開発は小規模開発が多く新しい開発者への知識の継承が困難、などの理由が考えられます。最も重要かつ効果的な「確実な入力と出力の制御」をセキュリティ対策として導入していないことも一因でしょう。インジェクション攻撃に対する防御がセキュアなプログラムに最も重要であることは、脆弱性と攻撃の数からも明らかです。

現在、防御的プログラミングはプログラミング原則の1つとして考えられ、セキュアプログラミングとも呼ばれます。安全なソフトウェアの構築には欠かせない基本原則なので確実に押さえておく必要があります。

 

セキュアプログラミング

「防御的プログラミング」はリアクティブ(受動的)なイメージの呼び名です。より高いレベルのセキュリティを確保するには能動的/積極的な行動/考え方が欠かせません。何か問題が起きるたび、パッチワーク的に問題を解決するのではより高いセキュリティレベルの達成は不可能です。プログラムも受動的、問題が発生するたびに特定の問題を解決していたのではキリがありません。プログラム中の問題を完全に除去することは不可能です。

リアクティブよりプロアクティブな対策であることを強調するには「防御的プログラミング」より「セキュアプログラミング」です。このため「セキュアプログラミング」という別の呼び方が考案されたのではないかと思われます。

最近では「セキュアコーディング」とコードを書くことに重点をおいた名前が使われることが多いと思います。この方が分かりやすい、と考えられているのだと思います。防御的プログラミング、セキュアプログラミング、セキュアコーディング、基本的にどれも同じ概念と考えて構いません。

セキュアプログラミングの要素には様々な要素があります。個々の要素や概念は難しくありません。WikipediaのDefensive Programming(セキュアプログラミング)を参照すると以下のような要素を挙げています。

  • Intelligent source code reuse(コードの品質を確認して再利用)
  • Secure input and output handling(入力バリデーション、安全な出力)
  • Canonicalization(正規化。標準形式に変換してから処理)
  • Low tolerance against “potential” bugs(バグとなる可能性のあるコードをできるだけ許容しない)
  • Other techniques
    • unchecked use of constant-size structures and functions for dynamic-size data(バッファーオーバーフロー対策)
    • Encrypt/authenticate all important data transmitted over networks(暗号化と認証を利用)
    • All data is important until proven otherwise.(バリデーションしたデータ、信用できるデータ以外は信用しない)
    • All data is tainted until proven otherwise.(バリデーションしたデータ、信用できるデータ以外は汚染されていると考える)
    • All code is insecure until proven otherwise.(全てのコードは安全だと証明されない限り、安全でない。盲目的に信用しない)
    • If data are to be checked for correctness, verify that they are correct, not that they are incorrect. (ホワイトリストを利用)
    • Design by contract (参考:エンジニア必須の概念 – 契約による設計と信頼境界線
    • Assertions(プログラミング言語のassert機能を利用)
    • Prefer exceptions to return codes(戻り値より例外を優先)

CERTのセキュアコーディングプラクティス TOP 10では以下の要素を挙げています。

参考:セキュアコーディングプラクティス TOP 10の日本語訳

  1. Validate input(入力バリデーション)
  2. Heed compiler warnings(コンパイラ警告を無視しない。解析ツールも使う)
  3. Architect and design for security policies(設計、デザインにセキュリティポリシーを反映する)
  4. Keep it simple(できるだけ単純にする)
  5. Default deny(デフォルトで拒否する)
  6. Adhere to the principle of least privilege(最小権限原則を守る)
  7. Sanitize data sent to other systems(出力は安全にエスケープ/サニタイズする)
  8. Practice defense in depth(縦深防御/多層防御を実践する)
  9. Use effective quality assurance techniques(品質保証テクニックを利用する)
  10. Adopt a secure coding standard(セキュアコーディング標準を採用/作成する)

CWE/SANS TOP 25 Most Dangerous Errorsでは、一般的なセキュアプログラミングの要素としてMonster Mitigations(怪物的緩和策※)を挙げています。※緩和策はISO 27000の定義では「セキュリティ対策」です。

  1. Establish and maintain control over all of your inputs.(入力バリデーション)
  2. Establish and maintain control over all of your outputs.(出力を安全に制御)
  3. Lock down your environment.(不即の事態に備え、環境をロックダウンする)
  4. Assume that external components can be subverted, and your code can be read by anyone.(外部データは汚染されていると仮定、コードは誰でも読めると仮定する)
  5. Use industry-accepted security features instead of inventing your own.(ベストプラクティスを採用する)

OWASP Secure Coding Practices Quick Reference Guide(PDF)では以下の項目を挙げています。

参考:OWASP Secure Coding Practices Quick Reference Guideの日本語訳

  • Input Validation(入力バリデーション)
  • Output Encoding(出力のエンコーディング/エスケープ)
  • Authentication and Password Management(認証とパスワード管理)
  • Session Management(セッション管理)
  • Access Control(アクセス制御、認可)
  • Cryptographic Practices(暗号)
  • Error Handling and Logging(エラー処理とログ)
  • Data Protection(データ保護)
  • Communication Security(コミュニケーション)
  • System Configuration(システム設定)
  • Database Security(データベース)
  • File Management(ファイル管理)
  • Memory Management(メモリ管理)
  • General Coding Practices(一般コーディングプラクティス)

OWASPのガイドラインで特徴的なのは”Output Encoding”です。エンコーディングとはエスケープの概念を拡張した考え方で、エスケープ+αと言えるものです。仕様としてエスケープすべき物だけをエスケープするのではなく、より安全にエンコードする、という考え方です。私もこの考え方を強く支持します。

CERT, SANS, OWASPのセキュアプログラミングガイドはPCI DSS規格で参照すべきベストプラクティスのソースとしても指定されています。PCI DSSはクレジットカード、デビットカードなどを扱う開発会社の場合は必須規格です。つまり、これらの会社にとってはCERT, SANS, OWASPのセキュアプログラミングガイドはアプリケーションに実装すべきセキュリティ対策のガイドラインになります。

言葉は異なりますが、「入力はバリデーションしなさい」「ホワイトリストを利用しなさい」「安全な出力を行いなさい」「ベストプラクティスを採用しなさい」「信用/安全とは確認するものである」といった所が基本的な考え方です。

「ベストプラクティスを採用する」には困った問題もあります。ベストプラクティスと思われている物でも、実際にはそうではない物(プリペアードクエリさえ使っていれば安全、など)があったり、実装過程であったりするものあります。またセキュリティ対策は危殆化(徐々に悪くなっていく。暗号、ハッシュなど)する物もあります。新しい攻撃手法で従来の手法(暗号通信の圧縮など)が危険になる場合もあります。従来より良い防御策が考案※される場合もあります。真贋を持つ事が大切です。

※ 実際、最近のOWASPサイトの改訂でJavaScriptエスケープの推奨方法が更新されています。まだこのブログでも紹介していないので、できれば近日中に紹介したいです。

 

 

まとめ

書き始めはもう少し詳細に「ざっと歴史を振り返る」つもりでしたが、いつものように書かないブログになってしまいました。申し訳ないです。

システム開発者や運用者は攻撃者のレベルに追いつけているか?この問いは重要です。追いつけていないと思います。だからこそセキュアプログラミング(防御的プログラミング)が重要になります。CERTを運営しているカーネギーメロン大学はCMMI(Capability Maturity Model Integration – 能力成熟度モデル統合)も開発しています。 セキュアな開発を実現するには、一足飛びで実現することは困難である、とする現実から組織を段階的にセキュアな開発を行える組織に変えていく方が現実的との考えから開発したと考えています。

セキュアな開発を一足飛びに実現することは困難ですが、セキュアプログラミングの概念は難しくありません。セキュアプログラミングの実践は程度の差こそあれ、直ぐにでも取りかかれます。しかし、実践しているソフトウェア開発組織は多いでしょうか?あまり多くないように思えます。現在のWebアプリケーション開発フレームワークは、不十分であっても、バリデーション機能があるものがほとんどです。積極的に活用しましょう。利用可能で信頼性の高いライブラリを利用しましょう。機能が無い場合は注意深く構築しましょう。

プログラム内部の一つ一つの関数/メソッドでセキュアプログラミングを実践することも重要ですが、一番重要なのはアプリケーション全体を守ることです。アプリケーション全体を守るには、アプリケーション境界、つまりアプリケーションへの入力と出力時にセキュアプログラミング(入力バリデーション、出力の安全化 – エスケープ(エンコード)、安全なAPI利用、バリデーション)することが重要です。

セキュアプログラミングの概念は簡単ですが、実践されていない、というより理解されていない(?)場合も多いように感じます。安全なソフトウェア開発にセキュアプログラミングは欠かせません。

関数一つ一つにチェックを入れていたら遅くなって実用に問題がある、と思うかも知れません。この問題はCode Completeにも記述されています。契約プログラミングをサポートする言語では簡単にこの問題を解決できます。契約プログラミングを原理主義的に実践すると、アプリケーションレベルでの入力/出力のチェックしか残りません。しかし、これでは危険なので適度なレベルで「縦深防御」(多層防御)を導入することを忘れないようにしてください。

最後に、ソフトウェアのセキュリティで最も重要な対策は信頼境界線での境界防御です。まず境界防御を行い、縦深防御/多層防御を行います。とても重要なので、どこにどのような信頼境界線があるのか、意識しながらプログラミングしてください。

 

参考:

Comments

comments

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です