Mozilla のセキュリティ評価と最良実践ガイド
草稿 3 - 2002年5月17日
キーポイント
- 製品への後付けでは優れたセキュリティを達成できません。設計の段階からセキュリティを考慮しなければならないのです。
- 製品のどこにあるバグでもセキュリティ上の脆弱性を引き起こし得ます。セキュリティバグは必ずしも PSM (Personal Security Manager) や ScriptSecurityManager で起こるわけではありません。
- 安全なコードを書くことは正しいコードを書くことの不可欠で必要な一部分です。メモリーリーク、初期化されていない変数への参照などを含むコードをチェックインしないでしょうし、レビュアーとしての承認を与えることもないでしょう。同様に、バッファオーバーラン、クロスサイトスクリプティング問題や以下に記述されている誤りを含むコードはチェックインを認められないものなのです。
- 要するに、セキュリティは全ての人の責務なのです。
問題
目標: 何から保護するのか。
「セキュリティ」はかなり漠然とした用語です。「プライバシー」もそうです。より明確に、Mozilla に関するコードを書くとき、何から保護する必要があるのかを以下に示します。
- 任意のコードの実行 / 一般的なファイルシステムへのアクセス - セキュリティ欠陥についての「至高の探索目標」。もし攻撃者が攻撃者の選んだ任意のコードによってユーザのメモリーに書き込んで実行できるならば、攻撃者はユーザのシステムを制御して、思いのままにどんなデータでも読んだり、修正したり、削除したり、ユーザがアクセスした他のマシンにアクセスしたり、ネット上でユーザに扮したり、攻撃されたマシンを更なる攻撃の拠点として使うことができます。これは私たちのソフトウェアに対する最も危険な種類の攻撃です。任意のコードを実行することとユーザのファイルシステムへの一般的な読み書きのアクセスとが同等であることに注意してください。一方が他方を容易に引き起こします。
- 任意のコードについてもう少し - 以上に記述された攻撃はバッファオーバーフローや他の捕らえにくい脆弱性の存在を必要としていません。ユーザを説得したりだましたりして、攻撃者の FTP サイトからトロイの木馬のプログラムを明確にダウンロードして実行させるのと同じくらい簡単かもしれません。ユーザの愚行の全てを阻止することはできませんが、いつ潜在的に危険なコードをダウンロードしていたのか、そしてそのコードがどこから来ているのかをユーザに認知させることはできます。私たちはユーザがダウンロードが行われたことを知るようにし、そのダウンロードを信頼するのかどうかを判断できるだけの十分な情報をユーザに与えるべきです。
- 特権の奪取 - しばしば信用証明書を手に入れずに、攻撃者はユーザの特権や信用証明書を利用します。例えば、一つのブラウザのウィンドウで銀行のサイトにログインし、二番目のウィンドウで攻撃者のページを訪れることになったと想像してください。もし攻撃者が口座振替を求める銀行のウィンドウ内のフォームに送信できるならば、実際にパスワードを手に入れずに、攻撃者は盗むために証明書を使っていたことになります。
- 制限されたローカルファイルへのアクセス / キャッシュの読み込み - 時々、攻撃者はファイルシステムへの一般的なアクセスをせずにユーザのシステムに悪意のあるコード(主として JavaScript)を埋め込めます。時々、Mozilla は攻撃者が Cookie ファイルやブラウザのキャッシュなどの特定の場所内のディスクにスクリプトを保存するのを助けてしまいます。
- 情報漏れ - Mozilla はユーザのはっきりした同意なしに、ユーザのメールアドレス、実名やその他の身元を確認できる情報を漏らすべきではありません。
- なりすまし - もし攻撃者があなたの銀行のログインページにそっくりなページを作成できて、そこであなたにパスワードを入れさせたならば、攻撃者はあなたの銀行の預金口座を盗んだことになります。世界中のどんな暗号作成法でもこの攻撃を妨げません。ただ注意深くユーザインターフェイスを設計することによってのみ、攻撃が難しくなるでしょう。
- サービス拒否 - ブラウザをクラッシュさせたり、無限ループでウィンドウを開かせたり、多くのメールを送りつけることは全てサービス拒否攻撃の一形態です。もしデータの損傷や他の永久的な損害を引き起こさなかったり、システムを危険にさらされた状態にしたままにしているならば、いらいらしている間、私たちはこれらの危険な弱点を突く手法について考えません。これらに対する最もよい解決策は単に再び不愉快なサイトに行かないというものです。
何から保護しないのか。
最も安全なコンピューターとは電源が切られ、プラグが抜かれて、コンクリートの中に埋められたものです。セキュリティと機能性は常に競合しているので、保護するものとしないものとしないものとを区別する必要があります。 特に:
- ダウンロードされたネイティブコード - ウェブページやメールのメッセージでユーザが直面するかもしれない全ての事からユーザを守ることができますし、するべきです。しかし、ユーザがダウンロードして実行した実行可能ファイルからユーザを守ることはできません。ネイティブな実行可能ファイルではオペレーティングシステムが守らせることを超えてアクセスをコントロールすることはできません。一般にバイナリの実行可能ファイルはユーザができることなら何でもできます。セキュリティホールやだまされやすいユーザに MyTrojan.exe をダウンロードするように誘うことを通して、一度攻撃者がネイディブコードをユーザのマシンに埋め込むと、攻撃者は勝利して私たちは敗北したことになります。これが、明確にそして説明を受けた上でユーザがネイティブコードをダウンロードして実行する決定をしなければ、それを行えないように一生懸命努力してきた理由です。
- インストーラーファイル:XPInstall を通して送られたファイルはユーザができることなら何でも可能で、これを制限する方法はありません。インストールした後、取るべき策はないので、ユーザはインストールに同意する前にインストールするファイルの製作者を信用しなければなりません。
- プラグインは、同様に、一度インストールされると、正にどんなことでもできます。
- 物理的なアクセス - もし攻撃者(あるいは詮索好きな家族の一人)があなたのマシンのキーボートのところに座っているならば、その人はあなたのマシンに対して何にでもアクセスし、何でもできます。ログインを必要とする OS を使ったりディスクやファイルレベルの暗号化ソフトを使ったりすることで保護が得られます。これらの何一つも Mozilla プロジェクトの責務ではありません。
要するに、主な目標はウェブページを見たり、メールを読んだりするという行動の中で直面する攻撃を防ぐことです。他の攻撃源もありますが、重大な関心事ではありません。
課題
Mozilla はオープンソースなので、私たちはセキュリティ問題を発見する上で明らかに有利な立場にあります。問題がないかコードに目を通すことのできる人の数に制限を設けていません。同時に、克服しなければならない明確な課題があります:
- Mozilla は多くの非常に異なったタイプのアプリケーション内に組み込まれ、多くのタイプのユーザに配布されています。これは全てのユーザに対してブラウザを設定して最新の状態を保つ、精通した IT スタッフの存在を当てにできないということです。Mozilla を基にした製品の全てのユーザへのパッチの配布が遅くて難しいかもしれないということでもあります。
- Mozilla は分散したやり方で開発された非常に複雑なアプリケーションです。複雑さはセキュリティの敵です。Mozilla は、もしかするとほとんど協調関係のない異なる集団によって書かれたかもしれないような、モジュール間の相互作用の中にあり、そこではセキュリティ問題がしばしば現れます。それが、他のモジュールによって不適当なデータが与えられたときでさえも、互いのモジュールが正しく動作するように設計しなければならない理由です。
- Mozilla のユーザインターフェイスはウェブコンテンツを作成するのに使われる言語と同じ言語(XML、HTML や JavaScript)で書かれています。このため信頼できないウェブコンテンツを信頼できるユーザインターフェイスのコードと混同しやすいのです。
- ユーザの中には経験のないコンピューターユーザがたくさんいる可能性があり、そのようなユーザは対話形式のウェブコンテンツに関係する危険を理解していません。これはユーザの判断をできるだけ少なくすることに頼らなければならないということです。Edward Felten が言ったように、
「踊る豚かセキュリティかの選択肢が与えられると、ユーザはいつでも踊る豚を選ぶでしょう。」
【訳注: この Edward Felten の発言については、Securing Java の How Does Java Security Stack Up? (Ch. 1, Sec. 7) の Figure 1.8 を参照してください】
解決策
以下が安全な機能を設計し、問題を未然に防ぎ、ありふれた落とし穴を捜す方法についての Mozilla のプログラマ、レビュアーやユーザインターフェイス製作者のためのガイドラインです。
セキュリティの黄金律:どんな入力源であっても信用してはいけません
偏執病的に聞こえますが、うまくいきます。あなたの入力がどこからのものなのか考えてください。ネットワークからのデータか、ディスク上のファイルか、ユーザの入力か、環境変数か、それともあなたの関数への引数か。もし入力がひょっとしたらあなたの管理外の源に由来するのならば、それがあなたの予想する書式であることを確実にするためにそれを確かめてください。全ての離れたサーバー、環境設定ファイル、コマンドライン引数が、損害を与えることに専念している悪質なハッカーによって作成されたと想定してください。どの入力の組み合わせでもコードが思いがけない振る舞いをするようにならないことを確かめてください。この方法で自分のコードを見ることで信頼性も向上します。セキュリティと信頼性は近い親類なのです。以下の点のほとんどは本当にこの基本規則の例です: 決して確認せずに入力が安全だと想定してはいけません。
Chrome JS
chrome を書くことはウェブページを書くことと非常によく似ており、セキュリティ上の懸念を引き起こします。しかし、chrome JS はネイティブなブラウザのコードの一部として考えられており、それができることについて何の制限も無いので、chrome に対する関心はより高いです。
覚えておくべき最も重要なことは全てのユーザの入力と、(更に重要なことに)URL を含む、ウェブからのデータを信用できず潜在的に悪意のあるものとして扱うことです。ウェブからのデータが chrome 内のどこで使われようとも、潜在的に危険な要素としてフィルターにかけなければなりません。
- HTML、XML または XUL をレンダリングしたり URL を読み込むところではどこでも、JavaScript が実行されるかもしれないということを覚えておいてください。HTML と XML の要素には <SCRIPT> タグを含めることができ、Mozilla ではスクリプトを実行させる javascript: と data: スキームを URL に使用できます。メインのブラウザウィンドウ内に表示されたウェブページでは、ブラウザのコンテンツ領域内の全てのものは信頼できないものとして扱われ、保護用の「サンドボックス」内で実行されるので、これは無害です。ダイアログや他の特別なウィンドウにはあてはまりません。あなたのダイアログは現在のウェブページやメールのメッセージに由来するデータを含んでいますか。それはページから取り込まれたリンクを表示しますか。もしそうならば、JavaScript をフィルターにかけるかスクリプトが確実に安全な環境内で実行されるようにしなければなりません。**例**
- 書かれている言語や書式ではなく、ファイルの場所や出所によってそれが信頼できるかが決定されます。ブラウザの chrome ディレクトリ内の全てがアプリケーションの一部のように扱われます。chrome ディレクトリ内に含まれるどんな JavaScript にも XPConnect を通じてネイティブなブラウザの API の完全な利用権限を持っており、任意のファイルの読み書きを含めて、コンパイルされた C++ コードができることが何でもできます。デフォルトでは、chrome ディレクトリ外のユーザのローカルなドライブ上のスクリプトファイルはブラウザの API の完全な利用権限を持っていません。それらはちょうどウェブからのスクリプトのように扱われます。最後に、ウェブスクリプトはデフォルトでは信頼できず、スクリプトができることは非常に限られています。これはスクリプトが HTML、XML または XUL ファイルのどれに入っていようと本当です。特権はスクリプトがどの種類のファイルに入ってくるかではなく、スクリプトがどこに由来するのかに基づいているので、単に XUL ファイル内にあるというだけでスクリプトにどんな特権であっても与えるべきではありません。
- 可能なときはいつでも eval() の使用を避けてください。eval() の原因となるので、setTimeout() と setInterval() への最初の引数として文字列を渡すことも避けてください。Eval は遅い上、悪意のあるコードを挿入して実行するのに役立つ手段を提供します。普通は別の手段があります。もし eval() を使わなければならないならば、関数に渡される文字列が予想した値を含むか確かめるようにしてください。
- chrome Javascript 内では、_content 内の全てが信頼できないので、_content への呼び出しに気をつけてください。渡した引数に気をつけ、結果に用心してください。_content 内のものが予想した型を持っていると想定しないでください。
特に:
- もし obj がコンテンツに由来するならば、obj.toString() の代わりに ToString(obj) を使ってください。ウェブページは obj の toString 関数を再定義していたかもしれません。
- 文字列だと予想していたとしても、コンテンツからのオブジェクトが文字列だと想定しないでください。
- コンテンツ内の関数を呼んだならば、ウェブページは渡した引数を読めることを思い出してください。このことはめったに問題にはなりませんが、ネットを通して送り返したくないどんな情報(例えば、ユーザのメールアドレス)でもページに渡さないよう注意してください。
- もし(chrome かコンテンツのいずれかに書き込むか、document.write を使うか、innerHTML に追加するか、または他の方法で)ウィンドウに文字列を書き込み、文字列の値がウェブページに由来するかもしれないのならば、それを書き込む前に文字列を html エスケープしてください。使用できる HTML エスケープ関数の例はこれです。
- リンクのターゲット、イメージや埋め込まれたオブジェクトの場所などとしてページやウィンドウに URL を書き、URL がウェブページに由来するかもしれないときはいつでも、URL を調べて、もしプロトコルが javascript: または data: ならばそれを書き出さないでください。これらの種類の URL を読み込むことでスクリプトの実行を引き起こし得ます。もし javascript: や data: を許可したいならば、実行するどんなスクリプトでも chrome スクリプトの全ての特権で実行しないことを確実にしてください。
javascript: URL では、(一般に HTML/XML の要素としてレンダリングされる)戻り値とスクリプトの実行による副作用の両方について気をつけてください。
C++
C や C++ は用途が広い一方で、セキュリティ上の誤ちを非常に犯しやすいです。特にある種の関数は非常に危険で、それらを避けるか非常に気を付けて使うべきです。この節では C や C++ のコードを書く時に避けるべき共通のセキュリティ上の落とし穴が記述されています。Mozilla のどこであってもこのような誤りはセキュリティ上の脆弱性を引き起こしうることを覚えておいてください。
バッファオーバーラン
バッファは連続したメモリーのブロックです。バッファオーバーランとはバッファが保持できるよりも多くのデータをバッファに書き込むことです。余分なデータはバッファに隣接したメモリー内の他の値を上書きします。それらの値が何であるかによって、それらを上書きすることで、攻撃者はプログラムの処理方法を変えたり、攻撃者の選んだ任意のコマンドを実行することさえできます。C や C++ はこれに対する組み込みの保護を提供していません。(JavaScript の場合は、更なるデータに対応するために必要に応じて動的に大きくなるバッファによって、提供しています。) 以下が必要最低限の例です:
void dangerousFunction(char* input)
{
char buf[100];
PL_strcpy(buf, input);
// 更なるタスク...
}
これは非常に危険な状態です。一般的には、どこであれサイズのチェックをしていない PL_strcpy(または標準 C ライブラリ関数 strcpy)を見たら、大いに気にしなければなりません。この例では、バッファはプログラムのスタックに蓄えられます。スタック上には他のローカル変数、引数およびこの関数が終了したときにプログラムの実行がジャンプする先の戻りアドレスもあります。もし攻撃者がこの関数への入力として 100 文字以上を渡したら、PL_strcpy 関数はバッファを満たし、そして戻りアドレスを含む、スタック上の他の値を上書きするでしょう。もし攻撃者がどこにバッファに関係のある戻りアドレスがあるのかわかったならば、攻撃者は PL_strcpy に戻りアドレスを攻撃者の選んだ値を設定させるような入力を作り上げることができます。ありふれた手法は攻撃者にユーザのマシンへの更なるアクセスを与えるアセンブリコードでバッファを満たし、戻りアドレスをバッファの最初に設定するというものです。
これが最も単純な例です。バッファオーバーランはこの例よりもいくらか捕らえにくい多くの形で起こります。一つには、オーバーランはスタックだけではなくヒープでも起こり得ます。つまり、malloc や new で割り当てられたメモリー空間はその上オーバーフローし得たり、隣接するデータが改変されうるのです。バッファに隣接するデータ層を予測することはより困難なので、ヒープ上のオーバーランは不正な収穫のためにはより困難です。しかし、弱点を突く手法は依然として可能です。
たいていのバッファオーバーフロー問題への解決策はバッファへコピーできるデータ量を制限することです。上の例では、これを行なう最も簡単な方法は PL_strcpy を PL_strncpy という境界のあるバージョンで置き換えることです:
void saferFunction(char* input)
{
char buf[100];
PL_strncpy(buf, input, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
// 更なるタスク...
}
私が PL_strncpy(buf, input, 99) を使うことができたことに注意してください。しかし、もし誰かがバッファのサイズを変更するとしたら、その上 PL_strncpy の呼び出しも変更することを思い起こさなければならないでしょう。コンパイル時に計算され、パフォーマンスへの影響のない、sizeof(buf) を使用する方がずっと安全です。もう一つの注意点は私が明示的にバッファの最後のバイトに null 文字をセットしたことです。(C ライブラリー版の、strncpy と同様に)PL_strncpy はバッファを null で終端させることを保証しないのでこれは必要です。
{PL_}strcpy に加えて {PL_}strcat、sprintf 一群の呼び出し、scanf や gets のような他の関数もバッファオーバーラン問題の危険にさらされています。完全なリストは下の **リンク** にあります。
書式バグ
printf()、fprintf()、sprintf() や snprintf() のような関数は書式関数として知られています。これらの関数は '%s' のような書式指定子を含むことのできる、書式文字列を引数に取ります。これらの記号に出会うと、関数を書式文字列にしたがって引数に基づき結果として生じる文字列へデータを挿入します。例えば、
printf("Today is the %ith day of %s", 5, "May");
は文字列 "Today is the 5th day of May" をコンソールに出力します。書式関数の危険性は攻撃者が書式文字列の内容に影響を及ぼすことができるときにやってきます。これはもし書式文字列が更なる '%' 書式指定子を含むならば、関数には余分な引数があることになり、関数はスタックから関数の引数やローカル変数を読み込み、それらを出力の文字列に含み始めるからです。このことによって攻撃者はあなたの関数の内部状態について情報を読み取ることができるかもしれません。それより悪いことに、'%n' 書式指定子は対応する引数に、出力した文字列に書き込まれたバイト数を書き込み、もし引数よりもパーセント書式指定子が多いならば、スタック上の他の場所に書き込みます。例えば戻りアドレスの上書きなどにより、攻撃者が実行中のプログラムを変えたり任意のコードを実行させることさえもできるという点で、バッファオーバーランに非常によく似た状態をこれによって作り出します。
** 例 **
これらの問題への解決策は信頼できないウェブコンテンツ(または、更に言えばユーザ)に書式文字列を指定させないということです。理想的には、書式文字列はハードコードするべきです。単純な例を挙げると、次のような printf の呼び出しは使用せず:
printf(str);
必ず以下のようにしてください:
printf("%s", str);
この方法で書式文字列に「錠をおろす」ことで脆弱性が排除されます。もし書式文字列をハードコードできず、それを含むデータが信用できない入力源に由来するかもしれないのならば、あらかじめフィルターにかけてください。例えば、もし書式文字列が %s 書式指定子を使用して、たった三つだけの異なる文字列を含むと予想するならば、そのように入力を検証できるかもしれません:
void buildString(char* formatIn, char* data1, char* data2, char* data3)
{
PRInt32 percentCount = 0;
for (PRInt32 j = 0; j < PR_strlen(formatIn); j++)
{
if (formatIn[j] == '%')
{
percentCount++; // % 書式指定子をインクリメントします
if (formatIn[j+1] != 's')
// %s 以外の書式指定子を見つけたので、
//エラーで強制終了します
return NS_ERROR_FAILURE;
if (percentCount > 3)
// 3を超える % 書式指定子があるので、エラーで強制終了します
return NS_ERROR_FAILURE;
}
}
char buf[1000];
snprintf(buf, sizeof(buf) -1, formatIn, data1, data2, data3);
buf[sizeof(buf)-1] = '/0'
}
snprintf を呼び出す前に、書式指定文字列が %- 書式指定子を 3 つだけ含み、更に %s だけを含むことを確かめるために調べています。バッファオーバーランを避けるために sprintf の代わりに sizeof() の制限と共に snprintf を使い、明示的に バッファを null で終端させていることに注意してください。
危険な関数
| 名前 | 危険水準 | 問題 | 解決策 |
|---|---|---|---|
| 名前 | 危険水準 | 問題 | 解決策 |
| gets | 非常に高い | 境界チェックなし | gets を使わないでください。代わりに fgets を使ってください。 |
| strcpy | 非常に高い | 境界チェックなし | strcpy は元の文字列が一定で、コピー先の文字列がそれを保持できるほど十分大きい場合に限り安全です。もしそうでなければ、strncpy を使ってください。 |
| sprintf | 非常に高い | 境界チェックなし、書式文字列攻撃 | sprintf を安全に使うのは非常に難しいです。代わりに snprintf を使ってください。 |
| scanf, sscanf | 高い | 境界チェックがない可能性、書式文字列攻撃 | 全ての %- 書式指定子が対応する引数の型と一致していることを確かめてください。境界チェックなしで '%s' 書式指定子を使わないでください。x が対応する引数のバッファのサイズである場合、'%xs' を使ってください。信用できず、検証されていないデータを書式文字列内で使用しないでください。 |
| strcat | 高い | 境界チェックなし | もし入力のサイズがよく分かっておらず固定されていなければ、代わりに strncat を使ってください。 |
| printf, fprintf, snprintf, vfprintf, vsprintf, syslog | 高い | 書式文字列攻撃 | 信用できず、検証されていないデータを書式文字列内で使用しないでください。もし書式文字列がウェブコンテンツやユーザの入力から影響されうるならば、これらの関数を呼び出す前に書式文字列を適切な数字と %- 書式指定子の型で検証してください。出力先のサイズ引数が正しいことを確かめてください。 |
| strncpy, fgets, strncat | 低い | null 終端ではないかもしれません | いつも明示的に目的のバッファを null で終端させてください。サイズ引数が正しいか確かめてください。目的のバッファに null 文字を加えるように注意してください! |
ファイルアクセス問題
競合
一時ファイル
パーミッション
シンボリック リンク攻撃