今のプログラムに足りないモノでセキュリティ向上に最も役立つ考え方のトップ2つ挙げなさない、と言われたらどの概念/原則を挙げるでしょうか?
私なら
- ゼロトラスト
- フェイルファースト
を挙げます。
極論すると、この2つ知って実践するだけでセキュアなソフトウェアを作れるようになるからです。この2つだけでは十分ではないですが、これを知って、実践しているだけでも開発者は今のコードより段違いにセキュアなコードが自分で書けるようになります。
もう一つ追加するなら
- 多層防御(縦深防御)
を加えます。これはゼロトラストとフェイルファーストから導き出せる概念です。ゼロトラストとフェイルファーストで検証を行うと自然と多層防御になります。多層防御はセキュリティ対策の基本ですが、特にソフトウェアでは実践されている、とは言えない状況です。
これら3つはとても有用な概念で単純明解な概念ですが、知られていなかったり、誤解されていたりすることが多い概念/原則と言えるかも知れません。
TL;DR;
ソフトウェアセキュリティに限らず物理的セキュリティ/ネットワークセキュリティも基本的な考え方は同じです。
- ゼロトラスト – 何も信頼せず、検証したモノのみを検証した範囲で信頼する
- フェイルファースト – 失敗するモノはできる限り早く失敗させる
例えば、以下のような考え方がゼロトラストとフェイルファーストです。
セキュリティが必要なビルなどでは入館時に「入館して良い人か?」全員を検証する。(ゼロトラスト)
宅配業者であると確認して入館させた人は社員と同じようにビル内の施設にアクセスできてはなりません。検証にはレベル/制限/検証の範囲があるが、それを無視して”チェック済みだからOK”としてしまう間違いは多い。一般に検証は多層構造で行います。
重要な部分に個別にセキュリティ対策、フロア/部屋/キャビネットなどに個人毎のアクセス管理機構など、があっても、外部からの訪問者は入館時に「入館を許可できるか?」検証する。敷地への立ち入りからチェックできるなら、門でチェックする。(フェイルファースト)
ゼロトラストとは?
ゼロトラスト(Zero Trust)とは言葉通りの意味で、「何も信用しない」です。プログラミングに於けるゼロトラストは以下になります。
- 他人のコード/データは信頼しない
- 自分のコードも信頼しない
- 正常に動作している(様に見える)コードも信頼しない
- プロセス/スレッドの外からのデータは信頼しない
正しい/妥当なコードなのか?正しい/妥当なデータなのか?これらをハッキリさせるには正しさ/妥当性を検証しなければ信頼できません。
どのようなコード/データなのか?検証なしに、どこの馬の骨のコード/データなのか分らないモノを信頼するのは自殺行為に等しいです。検証内容を正確に理解せず、単純に検証済みだから、と全面的に信頼してしまうのも同じく自殺行為に等しいです。信頼可能な範囲は検証した内容に限られます。
当たり前の話だね、と思うかも知れませんが大半のソフトウェア開発ではこれが当たり前ではありません。外部ネットワークと直接やり取りする公開Webアプリでさえ、この当たり前が出来ていないのが現状です。
- 中身の処理も知らずに出鱈目なデータを受け入れてもライブラリ/フレームワークなどが適当に処理してくれる
この考え方で攻撃されてしまった/攻撃可能になってしまった事例は枚挙にいとまがありません。ただのテキスト/数値データにプログラムが書けてしまい不正なデータを受け入れて攻撃された、データ不整合やエラーが発生してはならない部分でエラーが発生し問題なった、こういった事例は山程あります。
- 出鱈目なデータを処理させない責任を持つソフトウェアはアプリケーションです。
基本的にライブラリ/フレームワークは”汎用的”に作られており、できる限り多くのアプリケーションで利用できるよう緩いデータ検証機能しか持っていません。アプリケーションは”専用”のソフトウェアであり”特定のデータ”を処理するように設計/構築されます。
”ソフトウェアの役割”から
- 出鱈目なデータを処理させない第一の責任を持つソフトウェアはアプリケーションである
- ライブラリ/フレームワークなどの「無効なデータ処理防止/廃除機能は基本的には多層防御」であり、最初の防御機能ではない(責任は持つが第一の責任を持たない)
となります。
ソフトウェアに完全にゼロトラストを実装すると完全な「契約プログラミング/契約による設計」を実装した時と同じ状態になります。完全な「契約プログラミング/契約による設計」を実装した状態とは、全ての入力と出力およびアプリケーション状態を全ての関数/メソッドで完全に検証する状態になります。
何も信頼しない(ゼロトラスト)だけではITシステムは作れない
何も信頼しない/できない状態ではITシステムは作れません。勿論、プログラムも作れません。利用する全てのライブラリ/フレームワークのコードと動作を完全に検証し「信頼できるモノ」にすることは一般に不可能です。
例えば、利用する全てのコードを検証する場合、開発者が直接利用するAPI実装を検証するだけでは全く不十分です。そのAPIが利用するライブラリ、更にはOSのシステムコールの実装コード、コンパイラが出力するマシン語まで検証して初めて”完全に検証した”ことになります。
コードを全て検証する、は論理的には可能ですが現実的には不可能です。このOSなら、このライブラリなら検証なしでも信頼できるモノとして利用します。完全に検証していないコードを信頼できるモノとしても意図通りに動作するとは限りません。特に出鱈目なデータで正しくエラー処理が行われるか、は保証されません。そして出鱈目なデータに脆弱なコードは多く存在し、攻撃に利用されている現実があります。
コードが信頼できる、としても出鱈目なデータでの動作は保証されていない状態でプログラムを作ることになります。コードの動作を信頼しない状態でプログラムを作るとエラーチェックコードが冗長になり過ぎて、コードの見通しが悪くなる上にエラーチェックコードが原因で実行速度が極端に落ちます。
例えば、C/C++でプログラム開発をしている方ならValgrindと呼ばれるメモリ管理チェッカーを使ったことがあると思います。Valgrindはメモリ管理関数をオーバーライドすることにより、後付けで全てのメモリ管理状態のエラーをチェック可能にします。メモリ管理の状態をいちいちチェックするのでプログラムの実行が数倍、数十倍遅くなります。
もっと身近な例ではPCREライブラリ(Perl互換正規表現ライブラリ)の文字エンコーディングチェック機能があります。PCREはデフォルトでUTF-8文字エンコーディングのバリデーション(データの妥当性検証)を行います。これは不正なUTF-8文字エンコーディングが渡された場合の誤作動を防止する為のセキュリティ対策です。この文字エンコーディング検証機能が有効な場合、正規表現マッチの性能が数百倍以上遅くなることが知られています。(少なくともHaskellのPCREライブラリバインディングはUTF-8文字エンコーディングバリデーションを省略するオプションが指定可能。PHPは指定できません。)
このように何も信頼しない”ゼロトラストだけ”でITシステムを作ると、使い物にならないシステムが出来上がったり、使い物になっても最適化された状態の性能とは程遠い性能にしかならない、といった問題が発生します。
つまり、何も信頼しない(ゼロトラスト)だけではITシステムは作れないのです。特に出鱈目なデータでも「正しく動作すること」を保証するのは困難ですし、そもそも原理的に無理だったりします。
信頼するには妥当性を検証
何も信頼しないところから始めてセキュリティを構築しなければなりませんが、”ゼロトラストだけ”ではITシステムは作れないことが分かりました。この問題を解決するには信頼に足ることを検証し、信頼できるモノのコアとなるモノを作ります。
他人の書いたコードは完全に信用することはできません。自分の書いたコードと自分が取り扱ったデータは信頼できる状態にし、信頼するモノのコア、になるようにします。
- 自分が書いた全てのコードが信頼に足ることを検証する
- 自分が取り扱う全てのデータが信頼に足ることを検証する
信頼するモノにするためには、自分(自分達)が書いたコードは信頼に足ること、自分(自分達)処理するデータは妥当であること、を検証/保証します。他人が書いたコードは信用しないので、そのコードが想定する妥当なデータの範囲内で利用しなければなりません。(=仕様外の出鱈目なデータは絶対に他人のコードに渡さない。例外は他人のコードが完全かつ問題無い形で出鱈目なデータを処理することを保証できる場合、つまり完全に信頼できる場合に限る)
自分が取り扱うデータは全て信頼に足る(=仕様内のデータ)データであることを検証します。(セキュアコーディング原則1)
ソフトウェアの場合、”信頼境界”を定め、その境界を超えるデータの妥当性(完全性/機密性など)を検証します。既に書いた通り、他人のコードに対しては出鱈目なデータを絶対に渡さないように保証します。(セキュアコーディング原則7)
コード/データの妥当性を検証すれば、その”検証内容の範囲内”でコード/データを信頼することが可能です。他人のコードはその入力/出力/処理の仕様を把握し、その範囲内で利用するように自分のコードを書くようにします。
”検証したモノ(コード/データ)は無条件に信頼できる”としてしまうのはよくある勘違です。検証済みのモノであっても ”検証内容の範囲内”でしか信頼できない、つまり条件付きでしか信頼できない事に注意します。
セキュアコーディングでは自分のコードは信頼のコアとなるコードになりますが、自分のコードも信頼しません。このためセキュアコーディングの第7原則の「出力を無害化する」は入力バリデーションやロジックバリデーションと無関係に全て無害化します。
ライブラリ開発者とアプリケーション開発者の責任は同じではない
同じ開発者/プログラマといっても、ライブラリ開発者とアプリケーション開発者の責任は同じではありません。
ライブラリプログラマの場合、出鱈目なデータで出鱈目な動作になったとしてもライブラリ利用者の責任にすることも可能です。間違った使い方(≒出鱈目なデータ)を渡した開発者が悪い、とする仕様にしても何の問題もありません。
アプリケーションプログラマの場合、出鱈目なデータで出鱈目な動作になってしまう責任はアプリケーションプログラマが持ちます。出鱈目なデータを送ってきた送信者/利用者が悪い、出鱈目なデータで適当なエラーとならなかったライブラリが悪い、とすることは出来ません。
アプリケーションの場合、そもそもエラーとなってはならない箇所でエラーになること自体が問題になることがあります。例えば、出力時のエラー発生はアプリケーションに構造的問題がある、とされても仕方ありません。
非効率なセキュリティ対策だらけの現状
そもそも全てのコード/データは信頼できないモノであることが前提条件です。アプリケーションがコードとデータを信頼可能な状態してから利用する責任を持っています。にも関わらず
- 脆弱性/セキュリティ問題は呼び出された側に脆弱なコードがあったら、呼び出された側の責任である
とする考え方で大半のアプリケーションが設計/構築されています。この考え方がアプリケーション脆弱性の大きな原因となっています。
ライブラリなどは不正なデータでエラーになる事もチェックしていますが、基本的には妥当なデータで正しく動作するように設計/検証されています。
そもそも”仕様として”不正なデータではエラーにならず、不正な結果を出力してしまうライブラリも当たり前にあります。ライブラリ等は”汎用的”に作られており、どうしようもないデータ以外、何が妥当なデータであるか?関知しないのが普通です。
不正なデータでも「正しくエラーになる」ようなチェックを行っているつもりでも、チェック漏れで不正な動作を許してしまうプログラムも数えきれないくらいあります。
参考: ”組み合わせ爆発”により”不正なデータで正しくエラーになる”まで完全にチェックするような余裕は開発者にはない
アプリケーションでは、不正なデータによる不正な動作をライブラリなどの責任する、と考えるのは脆弱な考え方です。不正なデータでも「正しくエラーになる」ようなチェックを行っているつもりでも、チェック漏れで不正な動作を許してしまうライブラリは数えきれないくらいある、そもそもデータの正しさをチェックさえしないライブラリもある、と判っているからです。
ライブラリやフレームワークは汎用的に作られているので、特定用途のアプリケーション用に「正しくエラーになる」は構造的に実現できません。ライブラリやフレームワークだけで”訳の分らない不正なデータを与えられても、正しくエラーになる”という状態を達成することは不可能です。汎用的に作られているからです。一部の出鱈目なデータに対して正しく対応できても、アプリケーションで対策すべき全ての出鱈目なデータに対して「適切」に対応することは無理です。
例えば、不正な文字エンコーディング処理をライブラリ任せにすることは不可能です。 構造的に文字エンコーディングのバリデーションはアプリケーションの責任であり、ライブラリの責任することはできません。
結局、ソフトウェア製品としてユーザーに機能を提供するアプリケーション開発者が責任を持ってコード/データが正しく処理されることを保証(≒検証)するしかありません。
フェイルファーストとは?
フェイルファースト(Fail Fast)とは
- 失敗するモノはできる限り早く失敗させる原則
です。
多くの開発者は既にフェイルファーストを原則として実践していると思います。例えば、ユーザーの年齢を処理する場合、
- 数値であるか?
- 正の値である?
- 大きすぎる値でないか?
- ビジネスロジックで許可する範囲であるか?(年齢制限など)
といったチェックを、
- 年齢データを使った処理を行う前に行う
を実践していると思います。
しかし、フェイルファースト原則を全てのデータに適用しているWebアプリケーションはほぼ皆無なのが現状です。
全てのデータは検証/保証なしに信頼できません。
- プロセス/スレッドの外からのデータは信頼しない
これが原則です。
ゼロトラスト原則とフェイルファースト原則から、
- 全ての外部からのデータは検証し、検証エラーになるようなデータはできる限り早く廃除する
がベストプラクティスになります。 2017年版OWASP TOP 10では廃除するだけでは脆弱なアプリケーションである、と一歩踏み込んだセキュリティガイドラインになっています。今後はこれがWebアプリセキュリティの標準になると考えられます。
フェイルファーストとDAST
OWASP TOP 10 のA10 不十分なロギングとモニタリング では、出鱈目なデータを送信する動的なセキュリティ検査ツール(DAST – Daynamic Analisys Security Tool)からの攻撃に対して、検知/ログ/対応することを求めています。
DASTはアプリケーションが処理する入力に対して出鱈目なデータも送ります。フェイルファースト原則に従うとアプリケーション入力処理でこれらの出鱈目なデータは検知/廃除することになります。
DASTはデバッグフラグや隠し機能の有無をチェックする余計なデータも送ります。セキュアなアプリケーションはアプリケーションが期待する入力データのみでなく余計なデータ、Webアプリなら余計やHTTPヘッダー/クエリパラメーター/POSTパラメーターも検出できなければなりません。今のところこのようなセキュリティ対策はほぼ全てのWebアプリケーションで実装されていません。(ISO 27000は2000年から余計なデータの廃除を求めています)
新しいセキュリティ要求事項に見えるかも知れませんがフェイルファーストで考えると、余計なデータがあるデータは入力データとして問題である、と理解るはずです。ISOやOWASPを知らなくても、Fail Fast原則を理解していれば、必要なセキュリティ対策である、と開発者自身で定義できると思います。
ゼロトラストとフェイルファーストはほぼ万能
完全または万能と誤解されがちなセキュリティ対策に「SQLインジェクション対策にはプリペアードクエリを使う」「OSコマンドを実行する場合にはコマンドと引数を分離する」があります。これらは個別のセキュリティ対策として万能ではありません。万能と考えると著しく危険なセキュリティ対策です。何故危険なのかはゼロトラストとフェイルファーストで考えると理解ると思います。
ゼロトラスト、フェイルファーストも乱用/誤用が可能な概念ではありますが、正しく理解して適切に使えば万能の基本原則です。にも関わらず正しく理解されて利用されている、とは言い難いのが現状です。
ゼロトラストとフェイルファーストは単純明快かつほぼ万能な原則なので、ITシステムを作る場合には常に意識しておきたい原則です。
関連 :
ゼロトラストとフェイルファーストで考えるとCERT Top 10 Secure Coding Practices(CERTのセキュアコーディング トップ10原則)も自然と導き出せます。
多層防御(縦深防御)
リスク管理の基本は”SPOF(Single Point Of Failure)を作らない”です。どんなに完璧に見える対策でも、一箇所で全てのリスクに対応/緩和するのは高リスクです。
人は単純な間違いをします。全ての状況も想定できまねせん。プログラムでも「こんな間違いが!」「こんな状況が!」が原因で正しく動作しなくなることが普通にあります。
多層防御(Defense in Depth)も単純な概念です。
- リスクに対して複数の対応策を利用して対策する
物理的/ネットワーク的なセキュリティ対策では当たり前に実践されている考え方です。しかし、ソフトウェアでは当たり前に実践されているとは言えないモノが多いです。
ソフトウェア開発文化の問題
ソフトウェア開発では開発文化的に多層防御が行われない素地があります。これは構造化プログラミングから出来上がった
- 同じ処理はできる限り繰り返さない
が原因でしょう。一般に”同じ機能の共通化”は良いことです。しかし、セキュリティ対策に於て”同じ機能の共通化”は良いこととは限りません。セキュリティ対策ではリスクに対して複数の防御策(多層防御)で対応すべきだからです。
ソフトウェア開発/設計では”機能の構造化”を中心に考えます。手続き型/オブジェクト指向型/関数型などのプログラミングパラダイムに関わらず
- 機能を分割し、構造化する
がソフトウェア開発/設計の心臓部と言えます。
- 同じ処理はできる限り繰り返さない
- 機能を分割し、構造化する
この2つの概念が一緒になると”プログラム内部の一箇所にセキュリティ対策が集中”する”SPOF”だらけの”脆弱な構造”のソフトウェアになります。
機能でなく構造に着目した対策が必要
機能だけに着目し効率化を目指したソフトウェアだとSPOFを多数作ってしまいます。そこでソフトウェアの基本構造に着目した多層防御構造が必要になります。
まとめ
基本的なセキュリティ対策は
- ゼロトラスト
- フェイルファースト
の考え方で開発者自身が導き出せます。
歴史的/文化的な理由から無視されていることが多い
- 多層防御
も意識して取り入れると、効果的/効率的なソフトウェアセキュリティ対策を開発者自身が行えるようになります。セキュアコーディングの原則を知らなくても、自然と原則を実践できるようになります。
今のソフトウェアセキュリティ対策の基本と構造はベストプラクティスからは程遠い状態です。GDPR施行など、セキュアな構造でないアプリケーションを開発/運用するリスクは年々高まるばかりです。
基本さえ押さえておけば、裁判沙汰になっても不利にならないソフトウェアを作ることはそれほど難しいことはありません。