「フェイルセーフ」よく聞く言葉です。最近では「フェイルセキュア」1と言われることもありますが、基本概念は同じです。よく聞く言葉&簡単な概念ですが、割と広く誤解されている概念の1つに見えます。
フェイルセーフを一言で言うと
何かに失敗しても致命的な問題に至らないよう安全に失敗させる
これがフェイルセーフです。可能ならば「失敗/故障しても、失敗/故障の影響を受けないようする」場合もあります。ITシステムならRAID5や失敗時のリトライなどがこのケースです。
フェイルセーフ(フェールセーフ、フェイルセイフ、英語: fail safe)とは、なんらかの装置・システムにおいて、誤操作・誤動作による障害が発生した場合、常に安全側に制御すること。またはそうなるような設計手法で信頼性設計のひとつ。これは装置やシステムが『必ず故障する』ということを前提にしたものである。
となっています。
こんな単純な概念は間違いようがないでしょ?
と思うかも知れません。しかし、ソフトウェア開発では当たり前に誤解されています。
フェイルセーフの役割
フェイルセーフ機能は「何かに失敗しても致命的な問題に至らないよう安全に失敗させる」機能です。
「何かに失敗しても」とあるように基本的にフェイルセーフ機能は、
何らかの問題が起きた場合のバックアップ機能、万が一の時の機能
であると言えます。
2種類のフェイルセーフ – 頼れるフェイルセーフと頼れないフェイルセーフ
フェイルセーフ機能には2つのタイプがあります。
- 頼れるフェイルセーフ機能 – 何かに失敗しても、何事もなかったかのように機能し続けるモノ
- 頼れないフェイルセーフ機能 – 何かに失敗したら、致命的な問題を回避するため安全に失敗させるモノ
「頼れるフェイルセーフ」の場合、言葉通りフェイルセーフ機能に頼って構いません。例)RAID5ディスクの故障、メッセージキュー
「頼れないフェイルセーフ」の場合、言葉通り頼ってはなりません。できる限りになりますが、「そもそも何かに失敗しないようにする」ようにしなければなりません。例)出鱈目なデータによるSQLクエリエラー、出鱈目なデータ出力
フェイルセーフの前にやるべきこと
頼れないフェイルセーフ機能は、万が一の時の機能、であり本来は動作してはならない機能です。頼れないフェイルセーフ対策”だけ”に頼る設計にしてしまうのは誤りです。
当たり前の話ですね!
と思ったでしょう。しかし、この当たり前はソフトウェア設計では当たり前でなかったりします、特にWebアプリでは当たり前ではありません。
SQLインジェクション対策から学ぶダメなフェイルセーフ対策(セキュリティ対策)
SQLインジェクションはインジェクション攻撃対策の教科書として使うには最適な教材です。ここでもSQLインジェクションを題材にします。
以下のようなPostgreSQLのテーブルがあるとします。
CREATE TABLE mytable ( id int8 UNIQUE NOT NULL, name varchar(100) );
このテーブルにPHPを使ってデータを挿入するとします。SQLインジェクション対策としてプリペアードクエリを使い対策します。
$res = pg_query_params( $db, 'INSERT INTO mytable (id, name) VALUES ($1, $2)', array($_POST['id'], $_POST['name']) );
このコードなら、パラメーターから不正なSQL文をインジェクションされることはありません。SQLインジェクション対策として完璧です!
と言いたい所ですが、これだけでは完璧ではありません。何故でしょうか?
- $_POST[‘id’] が符号付き64ビット整数かつユニークでなければクエリエラーになる
- $_POST[‘name’]のも文字エンコーディング不正または100文字を超えるとクエリエラーになる
クエリエラーになるから問題ない!
と思うかも知れません。2種類のフェイルセーフ機能を思い出してください。
- 頼れるフェイルセーフ機能 – 何かに失敗しても、何事もなかったかのように機能し続けるモノ
- 頼れないフェイルセーフ機能 – 何かに失敗したら、致命的な問題を回避するため安全に失敗させるモノ
「クエリエラー」となる結果が「頼れるフェイルセーフ機能」であるなら
クエリエラーになるから問題ない!
であっても、「信頼性設計」とは言えませんが、構わないでしょう。
「クエリエラー」となる結果が「頼れないフェイルセーフ機能」であるなら
クエリエラーになるから問題ない!
はダメな設計です。「そもそもクエリエラーにならないよう対策する」必要があります。
クエリエラーでも問題ですが、出鱈目なデータの挿入も問題です。ほとんどのアプリケーションで出鱈目なデータがデータベースに保存されるのは大きな問題だと思います。クエリとして送信する前に、データをしっかりバリデーションしておく必要があります。
2004年にOWASP TOP 10が作られた当初から現在まで、SQLインジェクション対策として「入力データバリデーションを行う」としています。執筆時点の最新版である2017年版でも
Use positive or “whitelist” server-side input validation.
としています。
セキュリティ専門家であれば「信頼性設計」の概念を理解しているので、「そもそもクエリエラーにならないよう対策する」データバリデーションを必ず必要なセキュリティ対策要件に入れています。OWASP TOP 10でも一貫してこの要件をセキュリティ対策として記載してきています。
クエリエラーになるから問題ない!
ではなく、不正なデータはデータベースに出力する前のデータバリデーションで失敗させるのが基本です。
フェイルセーフ対策”だけ”に頼るアプリ設計/セキュリティ設計はNG
いろいろな要求仕様があるので、セキュリティ的/信頼性設計的にダメな設計、であってもOKな場合もあります。
どこかでフェイルセーフ機能が動作し致命的な問題(≒インジェクション問題)にならないなら問題ない!
とする考え方で作っても構わない時もあります。ソフトウェア設計に於てはライブラリなどを設計する場合に適用されることが多いです。
アプリケーション設計に適用すると問題のある設計方法です。アプリケーションとライブラリのセキュリティ設計では
- アプリケーションは信頼性を保証する責任がある
- ライブラリは信頼性を保証する責任がない
アプリケーション設計では、フェイルファーストによる早い段階で失敗(バリデーションにより失敗させる)を行います。
- フェイルファースト(Fail Fast)原則に則り、失敗するモノはできる限り早く失敗させる
- 基本的にはフェイルセーフ機能が動作しないしないように設計する
中にはどうしてもフェイルセーフ対策でしか対策できない、といった問題もあります。これは例外として扱います。基本は基本で変わらず、例外は例外です。
どこかでフェイルセーフ機能が動作し致命的な問題(≒インジェクション問題)にならないなら問題ない!
はセキュリティ設計として例外です。基本対策には成り得ません。
「フェイルセーフ対策を基本セキュリティ対策である」と勘違いしてしまう理由
結論から書くと、機能(コード)とデータは全く異なる視点からセキュリティ対策をする必要があります。機能(コード)だけ考えてセキュリティ設計をすると脆弱な設計になります。
プログラムを設計する場合、機能の構造化/抽象化、を考えて設計します。当たり前にプログラマーの皆さんが行っていることです。
機能の構造化/抽象化、は当然行うべきです。しかし、機能の事だけ考えてセキュリティ対策を実装してしまうとダメなセキュリティ設計になってしまいます。
- フェイルファースト(Fail Fast)原則に則り、失敗するモノはできる限り早く失敗させる
- 基本的にはフェイルセーフ機能が動作しないしないように設計する
この2つは高信頼性設計に欠かせません。
機能の中にセキュリティ対策を実装してしまうと、抽象化された機能の中、コードの奥深く、つまり
- フェイルファースト(Fail Fast)でない場所
でセキュリティ対策をする設計になりがちです。Fail Fastでない設計はセキュリティ的にはダメな設計です。ISO 27000のセキュリティ定義では”信頼性”もセキュリティ基本要素として定義されています。
コード(機能)とデータは異なる視点からのセキュリティ対策が必要です。機能はコードの奥深くに抽象化しますが、セキュアな設計ではデータはできる限り表面(ソフトウェアの信頼境界)で安全性を保証します。
まとめ
いろいろな誤解が絡まりあって、おかしな設計がまかり通っているのが現状です。著名なOSSソフトウェアでも、セキュリティ的にアンチプラクティス、なコードと設計であるケースは少くありません。少なくない、というよりセキュリティ的にはおかしな設計になっているモノが大多数です。
例えば、Railsはサニタイズ(ダメなデータを使えるデータに変換)に頼り過ぎです。サニタイズがダメな理由は2017年版OWASP TOP 10でサニタイズするアプリは脆弱アプリと認定されたことだけが理由ではありません。サニタイズはFail Fast原則に反しています。
サニタイズが絶対悪でNGではありませんが、基本はFail Fast原則のデータバリデーションを多層防御で行う、です。 図にすると以下のような構造になります。
注)アプリケーションレベルの境界防御は絶対的に必要なセキュリティ対策です。モジュール、関数・メソッドレベルの境界防御は必須ではありません。寧ろ全てのモジュール、関数・メソッドで境界防御をすべきではありません。詳しくは契約プログラミングを参考にしてください。
参考: 出力対策の半分はフェイルセーフ対策
- フェイルセーフ/フェイルセキュアの例として、「フェイルセーフ – 電源が落ちた時、鍵を開ける」「フェイルセキュア – 電源が戻って来た時、鍵を開ける」があります。どちらも「失敗(電源断)の時、安全に失敗させる」ことが目的です。違いは「失敗した時の対処方針」です。 ↩
Leave a Comment