セキュアなアプリケーションアーキテクチャ

(Last Updated On: 2018年8月8日)

セキュアなアプリケーションには共通したアーキテクチャがあります。基本的には防御的プログラミング(セキュアプログラミング)を行い、防御的プログラミングのテクニックの1つである契約プログラミングを実践したアーキテクチャがセキュアなアーキテクチャです。

アプリケーションのセキュリティ問題のほとんどはインジェクション問題です。インジェクション問題以外にもセキュリティ問題はありますが、ここではインジェクション問題のみを考慮します。

セキュアなアプリケーションのアーキテクチャ

セキュアなアプリケーションは境界防御と縦深防御(多層防御)の両方を実装します。契約プログラミングでは

  • 呼び出し側(caller)が呼び出される側(callee)の呼び出し時に契約/約束事を守る
  • 呼び出された側(callee)は呼び出された後の契約/約束事を守る

ことで実現します。

呼び出し側(caller)が呼び出される側(callee)の呼び出し時に契約/約束事を守る(入力のセキュリティ)

プログラマがアプリケーション内で作る関数やメソッドであれば、callerがcalleeの約束事を守らせることは容易です。問題は外部からの入力です。

アプリケーションの機能を呼び出すのは、ユーザーや外部システムです。ユーザーや外部システムがアプリケーションの機能を呼び出す際に、入力がアプリケーションにとって妥当であることを強制することはできません。このため、アプリケーションは入力が妥当であることを保証しなければなりません。入力が妥当であることを保証するには2つの方法があります

  • バリデーション – 入力が妥当である場合のみ受け入れる。それ以外はエラー。
  • サニタイズ – 入力を妥当な入力に変換する。エラーは発生させない。

入力に対するセキュリティ処理は基本的にバリデーションを行います。しかし、不特定多数かつ大量のユーザーが居る場合、バリデーションを行うとエラーログが大量になる、などの問題も発生します。大量のエラーログが問題になる場合、サニタイズを選択する事もあります。

入力バリデーションをしていれば問題なりませんが、不正なデータを変換するサニタイズには問題もあります。例えば、ASCIIコードの制御文字や不正な文字エンコーディングなどをサニタイズすると、危険になってしまう場合があります。”script”という文字列をブロックしていても、制御文字をサニタイズしている場合、”s¥0c¥br¥0i¥vp¥rt”はブロックされません。制御文字を取り去るサニタイズにより”s¥0c¥br¥0i¥vp¥rt”は”script”になります。サニタイズやブラックリストは原理的にリスクを高める方式と言えます。

ユーザーが不特定多数でも大量でもない場合はバリデーションを行わないよい理由はありません。大量のエラーログが問題にならない場合はバリデーションを行います。

呼び出された側(callee)は呼び出された後の契約/約束事を守る(出力のセキュリティ)

これには戻り値として正しい(妥当)な値を返したり、出力として正しい(妥当)な出力を行うこと、状態(オブジェクトの状態やグローバル変数、ファイルや環境変数などの値)が正しい(妥当)であることを保証する必要があります。

戻り値/出力が正しいことを保証するには、戻り値/出力を処理する処理系が誤作動しないようにします。この為には戻り値/出力で利用する変数には

  • エスケープ(エンコード)を行う
  • エスケープ(エンコード)が必要ないAPIを使う
  • 上の2つが不可能な場合はバリデーションを行う

が必要です。

状態が正しい(妥当)であることのチェックはケースバイケースです。プログラマであれば、どのような状態が正しい状態であるか知っているハズです。状況に合わせて正しい状態であることを確認します。

重要:セキュアなアプリケーションでは両方(入力と出力)のセキュリティ確保を行う

アプリケーションの境界は「アプリケーションへの入力とアプリケーションからの出力」です。ここでは全体として1つのアプリケーションとして動くシステム単位で議論していません。単体のソフトウェア(I/Oなどを使わずにプログラマ自身が完全に制御できるコード)の入出力、ここが信頼境界線になります。

Webアプリケーションのサーバー側ならサーバー側のコードのみが信頼できます。サーバー側のコードであっても、3rdパーティーのモジュールやライブラリは信頼境界線の外にあります。信頼することも可能ですが、信頼するには「論理的な根拠」が必要です。

入力と出力のセキュリティ対策は”独立したセキュリティ対策”であり、セットで”境界防御”になります。入力、出力のどちらかを対策すればセキュアなコード/アーキテクチャーとしてOKというモノではありません。入力だけ、出力だけのどちらでも不十分です。

インジェクション対策は入力と出力の両方で独立して行います。CERT Top 10 Secure Coding Practicesの一位の対策は入力バリデーションで、出力対策は以下のように解説しています。

7. 他のシステムに送信するデータを無害化する

コマンドシェル、リレーショナルデータベースや商用製品コンポーネントなどの複雑なシステムへの渡すデータは全て無害化する。攻撃者はこれらのコンポーネントに対してSQL、コマンドやその他のインジェクション攻撃を用い、本来利用 してない機能を実行できることがある。これらは入力バリデーションの問題であるとは限らない。これは複雑なシステム機能の呼び出しがどのコンテクストで呼 び出されたか入力バリデーションでは判別できないからである。これらの複雑なシステムを呼び出す側は出力コンテクストを判別できるので、データの無害化は サブシステムを呼び出す前の処理が責任を持つ。

CWE/SANS TOP 25の怪物的セキュリティ対策TOP5でも入力(1位)と出力(2位)のセキュリティ対策は独立した対策として明記しています。

OWASP Top 10(2013)でも一位の脆弱性であるインジェクション対策でも、入力バリデーションを推奨し、入力対策は独立したセキュリティ対策であるとしています。

 

  • Positive or “white list” input validation is also recommended, but is not a complete defense as many applications require special characters in their input. If special characters are required, only approaches 1. and 2. above will make their use safe.

1と2とはエスケープが必要ないAPIを使う、無い場合はエスケープする、という対策です。実践論の場合はこの順番で良いのですが、セキュリティの基礎教育としてはこの順序は逆の方が良いでしょう。

出力のセキュリティ対策とは独立して入力のセキュリティ対策を行うのがセキュアなアーキテクチャです。ISO27000(ISMS)は入力バリデーションを要求しています。PCI DSSではSANSやOWASPのガイドラインを採用するよう要求しています。これらのセキュリティ規格に準拠する場合、プログラムのセキュリティ対策として入力対策を行わないのは問題となります。

 

縦深防御(多層防御)の実装例

契約プログラミングを原理主義的に実装すると、アプリケーションのセキュリティ処理は

  • 外部からの入力
  • 外部への出力

の2箇所のみで行うことになります。アプリケーションのコードが完璧であれば、この2箇所のみで対策することにより完璧なインジェクション対策が可能です。しかし、完璧なコードを想定し他の場所でセキュリティ対策を行わないのはセキュアなアプリケーションではありません。論理的には出力対策だけでも防げますが、複雑な処理を伴う出力(特にHTML)で出力対策のみ完全に対策するのは困難です。

セキュアなアプリケーションアーキテクチャでは、アプリケーション境界のみでセキュリティ対策を行うのではなく、さらに内部の部分でも

  • フェイルセーフ
  • 縦深防御(多層防御)

を実装します。セキュリティの基本要素には完全性(Integrity)もあります。データベースに保存されるデータには完全性が要求されます。アプリケーションコードに問題があってもデータベースに不完全/不正なデータが保存されないよう対策するのが得策です。

従って、データベースに保存する場合にもデータをバリデーションします。リレーショナルデータベースを利用していれば、参照整合性やNULL制約などは自動的にバリデーションされます。アプリケーションではこれら以外の完全性をバリデーションします。例えば、

  • 年齢は0才から130才
  • 体重は1kgから180kg

などを満たしているかチェックしてから保存します。(体重は切り上げとする、180kgを超える体重の場合181kgまで許可し181kgはそれ以上を表すとする、などの工夫が必要な場合もあるでしょう)

ほとんどのリレーショナルデータベースはCHECK制約やトリガーをサポートしているので、データベース側でバリデーションしてエラーにできます。アプリケーションが仕様として許可するデータ制約はアプリケーションのコードで確認可能ですが、より高い安全性が必要なアプリケーションの場合はデータベース側でも確認することが一般的です。

CHECK制約やトリガーをサポートしていないキーバリュー型のデータベースなどを利用している場合はアプリケーション側で実装せざるを得ません。一般的なRDBMSに比べ縦深防御が無い状態になり、リスクが高いといえます。

リレーショナルデータベースの様に外部システムの機能を利用できる場合もあります。しかし、大きなプログラムなってくると、モジュール/部品単位で多層防御する必要性も出てきます。これはセキュアなプログラムを作るためには「簡単にする」事も大切だからです。一枚岩のように一体化させて密結合したプログラムの安全性を維持するのは簡単ではありません。

 

まとめ

セキュアなアプリケーション/プログラムを作るにはアプリケーション/プログラム単位で「境界防御」(入出力の確実な制御)が必須です。入力と出力のセキュリティ対策は独立したセキュリティ対策として実施する必要があります。プログラムが大きな場合は適当な粒度で分割して管理する方が簡単です。そして密結合でなく疎結合になっている方が理解り易く、管理もし易いです。これは普通にプログラムを作る場合の機能設計と同じです。

入力バリデーションはアプリケーション仕様でありセキュリティ対策ではない、とする議論もあるようですがこれは誤りです。ISO27000の定義では入力バリデーションは明らかに必要事項として要求されている「セキュリティ対策」です。またセキュリティ仕様はアプリケーションの仕様のサブセットであり「アプリケーション仕様だからセキュリティ仕様(対策)ではない」は論理的に矛盾します。

全てのセキュリティ仕様をアプリケーションに入れる必要はありません。しかし、入力バリデーションはCERT/CWE/SANSのTOP1のセキュリティ対策の仕様であり、アプリケーションの仕様として採用しない/したからと言ってセキュリティ対策になったり/ならなかったりするような物ではありません。セキュリティ対策の優劣は時代と共に変化する場合もありますが、セキュリティ対策は常にセキュリティ対策です。※参考

手段が目的化していると、本来の目的を見失うことがあります。入力対策がセキュリティ上重要でない、と勘違いしている方は「手段目的化の罠」にはまっている方が多いと思います。「手段自体は間違っていない」のですが「目的達成を遠回り」していると思います。

投稿者: yohgaki