[This is a Japanese translation of Some thoughts on security after ten years of qmail 1.0]
Daniel J. Bernstein
Department of Mathematics, Statistics, and Computer Science (M/C 249)
University of Illinois at Chicago, Chicago, IL 606077 045, USA
djb@cr.yp.to
CSAW’07, November 2, 2007, Fairfax, Virginia, USA.
Public domain.
qmailソフトウェア・パッケージは、インターネット上で広く利用されている メール配信エージェントであり、これは 1997年からセキュリティ保証がついている。 本論文では、qmail の作者がその歴史と qmail のセキュリティに配慮した アーキテクチャを考察する。qmail が満たせなかったパーティショニングの 標準について明示し、その失敗にもかかわらず qmail が生きのこることを 可能にしたエンジニアリングについて分析する。 また、セキュアなプログラミングの将来に関していくつかの結論を述べる。
分類:
D.2.11 [ソフトウェア・エンジニアリング]: ソフトウェア・アーキテクチャ - バグの除去、コードの除去
D.4.6 [オペレーティング・システム]: セキュリティと保護
H.4.3 [情報システムとアプリケーション]: 通信アプリケーション - 電子メール
一般用語:
セキュリティ
キーワード:
バグの除去・排除、コードの除去・排除、信頼されるコードの除去・排除
すべてのインターネット・サービス・プロバイダは、 MTA (メール転送エージェント) を走らせている。MTA はローカル・ユーザからの メールを受け取り、SMTP (インターネットでは Simple Mail Transfer Protocol) を利用して 他のサイトにメールを配信し、また SMTP を使って他のサイトからメールを受信する。 私は1995年に Eric Allman の "Sendmail" ソフトウェアにあるセキュリティ・ホールに うんざりしていたため、MTA である qmail を書き始めた。Sendmail は、当時 インターネット上でもっともポピュラーな MTA だった ([4] を参照)。 以下は私が 1995年12月に qmail のドキュメントに書いた文章である:
数ヶ月に一度、CERTは「今度のSendmailセキュリティ・ホール」を アナウンスしています。これは、ローカルあるいはリモートのユーザが そのマシンの完全な制御を可能にするものです。私は、さらに多くの 穴が発見されるのを待っていると確信しています。 Sendmailの設計は、41000行にわたるコード中のいかなる些細なバグも 甚大なセキュリティ・リスクとなることを示しています。これ以外の ポピュラーなメーラ、Smail とか、メーリング・リスト・マネージャである Majordomoも同じくらいひどいようです。
1996年と1997年のあいだに、14個のSendmailセキュリティ・ホールが 発見された。私はこれ以降数えることをやめ、ついには気にかけることも やめるようになった。検索したところでは、もっとも最近の Sendmail 緊急セキュリティ・リリースは 2006年3月のバージョン 8.13.6 である ([10], 「リモートの認証されていない攻撃者がSendmailプロセスの権限で 任意のコードを実行できる可能性」)。 20年以上にわたるSendmailのリリースがリモートから攻撃可能であったあとで、 いま最新版のリリースがリモートから攻撃可能でないと 言い切れる人がいるだろうか? Sendmailセキュリティ・ホールがアナウンスされる ペースは遅くなってきたとはいえ、このことはすでに Sendmailの穴を通じて侵入されてしまったシステム管理者の 助けにはならない。
1995年12月に、私は qmail のコードを真面目に書き始めた。 ちょうど代数的数論のクラスを終えたあとで、私にはすこし 時間の余裕があった。最終的なひと押しは、私が同僚にした 約束だった。私は彼のために、大規模なメーリング・リストを 作ってやると約束していたのである。Sendmailでは、私がしたかった 簡単なリスト管理はできなかったし、Sendmailは大量の受信者に (逐次的に!) メッセージを送るのに永遠の時間がかかるかと思われた。 Sendmail自身の信頼性・セキュリティ問題を置いておくとしても、である。 1995年12月7日の版のqmailは、14903語のコードを有していた。 この測定には以下の方法を用いている:
cat *.c *.h | cpp -fpreprocessed \ | sed 's/[_a-zA-Z0-9][_a-zA-Z0-9]*/x/g' \ | tr -d '\012' | wc -c
(これ以外の測定方法でもだいたい似たような結果になった。) 1995年12月21日の版では、36062語だった。1996年1月21日には qmail バージョン 0.70 になり、これは 74745語あった。 このバージョンを自分のコンピュータで数日間走らせてみたのち、 私はこれを公開し、qmailベータテストを始めることにした。 1996年8月1日に私は qmail 0.90 をリリースし、これは 105044語あった。 この時点で私はベータテストを終了した。1997年2月20日、 私は qmail 1.00 をリリースし、これは 117685語だった。 1998年6月15日に現行バージョンである qmail 1.03 をリリースした。 これは 124540語ある。これをやや派生させた netqmail 1.05 が コミュニティによって開発されており、これは 124911語ある。 私は qmail 1.0 リリースには 4つのバグが存在することを認識している。 比較のために述べておくと: 1996年3月にリリースされたSendmail 8.7.5 は 178375語ある。 1997年1月にリリースされたSendmail 8.8.5 は 209955語ある。 1998年5月にリリースされたSendmail 8.9.0 は 232188語ある。 これらのSendmailリリースでは、リリース・ノートに何百という数の バグが報告されている。Sendmailとqmailの間には、ユーザに見える いくつかの機能の差異がある。たとえば qmail の POPサポートや、 SendmailのUUCPサポート、qmailのユーザ制御可能なメーリング・リスト機能、 そして Sendmail の「リモートroot権限取得機能」 — 冗談ですってば! — など。 しかし、これらだけではこの複雑さの差を説明することはできない。 どちらのソフトウェアでも、コードのほとんどの部分は インターネット上の典型的なサイトで必要とされる核となる MTAの機能を実装している。フィンガープリントを利用した検出によると インターネット上の100万以上のサイトが SMTP サーバとして qmail 1.03 あるいは netqmail 1.05 を使っている。 第三者が運営しているサイトである qmail.org の報告によると:
多くの大規模インターネット・サイトが qmail を使用しています: USA.net の外向きメール, Address.com, Rediffmail.com, Colonize.com, Yahoo! mail, Network Solutions, Verio, MessageLabs (1億通/週の電子メールからマルウェアを検出する), listserv.acsu.buffalo.edu (大規模なリスト・サーブで、1996年からqmailを使用), Ohio State (米国で最大の大学), Yahoo! Groups, Listbot, USWest.net (米国西部の ISP), Telenordia, gmx.de (ドイツの ISP), NetZero (フリーの ISP), Critical Path (電子メールのアウトソーシング・サービス、 1500万件を超えるメール・ボックス有), PayPal/Confinity, Hypermart.net, Casema, Pair Networks, Topica, MyNet.com.tr, FSmail.net, Mycom.com, そして vuurwerk.nl。
何人かの著者が qmail に関する書籍を書いている: [7], [20], [14], [22]。 完全な統計をとることは難しいが、標本抽出が示唆するところでは qmailはインターネット上における相当量の正当なメールを 送信および受信している。
1997年、私は qmail の最新バージョンに検証可能なセキュリティ・ホールを 最初に見つけた人に $500 を進呈とを公表するという、珍しい 行動に出ることにした。たとえば、あるユーザがqmailを使って別のユーザの アカウントを乗っ取る方法などに対してである。このオファーはまだ 生きているが、まだqmailのセキュリティ・ホールを発見した人はいない。 私はここで進呈額を $1000 に引き上げることにする。
もちろん「qmailのセキュリティ・ホール」には、qmailの外の問題は 含まれない。たとえば NFS のセキュリティ問題や、TCP/IP のセキュリティ問題、 DNS のセキュリティ問題、.forwardファイル内のバグ、そして一般的な オペレーティング・システムのバグなどである。qmailがインストールされる前の システムがすでに脆弱である場合に、qmailを批判するのは的をはずしている! しかし、qmailがメッセージをネットワークに送る前に暗号化や 認証をしていないという批判は、それほど的をはずしてはいない。 もしかすると、暗号化は TCP/IP ではなく qmail のようなアプリケーションが 処理すべきなのかもしれない。しかし、暗号化は qmailセキュリティ保証の 対象外である。また、サービス拒否攻撃も明示的に対象からはずされている。 この問題はすべての MTA に存在し、明確に文書化されており、 またいくつかのメジャーなプロトコルを根本的に改訂しないことには 非常に対策が難しい。ある人々は、現在のインターネットにはこの改訂が どうしても必要だと主張する。そして私もそれに同意するが、 それは別の論文のテーマである。
qmailのセキュリティ保証が含んでいることは、ユーザはqmailを悪用して、 他のユーザのデータを盗んだり改竄したりできないということである。 もし他のプログラムも同様の基準に従っており、私たちのネットワーク接続が 暗号学的に保護されていれば、インターネット上で残っている セキュリティの問題はサービス拒否攻撃だけになる。
qmailはいかにして、このような前例のない水準のセキュリティを 達成できるように設計されたのだろうか? qmailはセキュリティ的に どれくらいうまくできており、どうすればもっとよくできたのだろうか? これと同程度のセキュリティ保証を自信をもって公表できるような、 他のソフトウェア・プロジェクトはどうしたらつくれるだろうか?
セキュリティに対する私の視点は、年を追うごとに厳しくなっている。 これまで巨額の資金と努力がセキュリティのために費やされるのを 見てきたが、これらの資金と努力のほとんどは無駄になっていると 感じている。ほとんどの「セキュリティ」研究は、過去の攻撃を 止めるのに費やされており、将来の攻撃をくい止めることに完全に 失敗しているか、脆弱でないソフトウェアを設計するにはほとんど 役に立っていない。これらの努力は、より長期的な価値をもつ 研究の努力を削いでしまっている。
後知恵で考えると、qmailの「セキュリティ」機構のうちいくつかは、 生半可なアイデアで、実際に何かの役に立ったわけではなく、それがなくても セキュリティは損なわれなかった。それ以外の機構は、 qmail の成功したセキュリティ記録に貢献している。本論文における 私の目標は、いかにしてこの差異を前もって認識することができるのかを 明らかにすることである — つまり、ソフトウェア・エンジニアリング技術は その長期的なセキュリティ上の成果に対して、 どのように評価されるべきなのか。
第2節では、 脆弱でないソフトウェア・システムの構築に向けた、 3つの方針を特に解説する。本論文の残りの節では、これらの 方針に関して、qmail が成功した点と失敗した点とを述べる。
ここで私が述べることの多くは、これまでに何度となく述べられてきたことである。 (この論文はソフトウェア・セキュリティに関する最初の論文ではないし、 qmailのセキュリティに関する最初の論文ですらない。) 元々の出典を記す時間をとらなかったことをお詫びしておく。
定義によれば、ソフトウェアのバグとは、ユーザの要求に反する ソフトウェアの能力のことである。定義によれば、 ソフトウェアのセキュリティ・ホールとは、ユーザのセキュリティ要求に反する ソフトウェアの能力のことである。したがって、すべてのセキュリティ・ホールはバグである。 (形式的な仕様や、セキュリティ・ポリシーの記述などを推進する人々は、いみじくも ユーザの要求を完全に得ることは非常に困難であると指摘するかもしれない — とりわけユーザがそこまで考えの及ばないグレー・ゾーンにおいてはそうである。 以下に述べるように、この複雑さはセキュリティ・ホールを除くことの 難しさにも影響を与えている。しかし、この複雑さがなくても 依然として困難は存在する。なぜなら、今日のソフトウェアは考えうる もっとも基本的なセキュリティ基準にさえ合致していないのだから!)
ここで、N語のコードにつき平均して 1個のバグが存在すると仮定しよう。 しかしコンピュータの中には 10000N語のコードが存在している。 すると、このコンピュータには約10000個のバグが存在するという不幸な結論に なってしまう。そしてこれらのバグの多くは、おそらくセキュリティ・ホールなのである。
セキュリティ・ホールがなくなるという目的のために、 私たちはどのように改善していったらよいのだろうか? そしてその進歩をどのように評価するのだろう? この節では 3つの回答を提示する。 また、この節ではコミュニティ全体がいかに自分自身の 足を引っぱっているかについても議論する。
最初の、そしてもっとも明白な回答は、バグの発生率を下げることだろう。 ソフトウェアの製作過程で生じるバグの割合は、 成果としてのコードを注意ぶかくレビューし、見つかったバグの数を レビューしたコードの量の関数とすることによって推定できる。 つぎに異なるソフトウェアの製作過程で生じるバグの発生率を比較し、 その発生率を下げるためにプロセス自体をメタ・エンジニアリングする。 (ときに、コード・レビューをすり抜けて微妙なバグが混じることがある。 しかし経験によると、すべてのユーザ要求を考慮したとしても、 全体のバグの発生率は、さほど微妙でないバグの発生率よりわずかに高くなるだけである。 エリック・レイモンドは、[17, Section 4: ``Release Early, Release Often''] において 『十分な数の目玉があれば、どんなバグも脅威ではなくなる』とコメントしている。)
バグの除去はソフトウェア・エンジニアリング研究における古典的なトピックのひとつである。 たとえば、よくあるソフトウェア製作過程に被覆テストを加えることにより、 バグの発生率は劇的に減少することが知られている。しかし、バグの発生率は ソフトウェア製作過程の早い段階における決定にも影響されるということは それほど知られていない。さらに詳しい議論については本論文の 第3節を参照のこと。
もう一度、10000N語のコードをもつコンピュータ・システムについて 考えてみよう。バグの発生率を N語につき1個の割合よりもはるかに減らせたとして、 それでもまだ 10000N語のコードからすべてのバグをなくすには程遠いとする。 どのようにしたらこれを改善できるだろう?
2つめの回答は、コンピュータ・システム全体におけるコードの量を 減らすことである。ソフトウェアの製作過程はあるコード量における バグの数に影響を与えるだけでなく、ユーザが欲する機能を 提供するためのコードの量にも影響を与える。コードの除去もまた、 ソフトウェア・エンジニアリング研究における典型的な課題のひとつである。 異なるソフトウェア製作プロセスによって作られたコードの量を比較し、 少ないコード量で同じことができるようなプロセスを メタ・エンジニアリングする。 さらに詳しい議論については本論文の 第4節を参照のこと。
また、バグとコード量の割合や、コード量と機能の割合を 個別に測定するのではなく、バグと機能の割合を測定することもできるかもしれない。 しかし、個別に測定することで異なるテクニックを目立たせることができるし、 コード量と機能の割合は、ソフトウェア製作にかかる時間を 予測するのにも有用である。また、バグとコード量の割合は デバッグにかかる時間を予測するのにも有用である。
ここで、より少ないコードで動くコンピュータ・システムを作り、 より少ないバグのコードを書けたとして、最終的にバグの数が 1000個以下のシステムを作れたとする。しかしここでも 多くのバグはセキュリティ・ホールである。 どのようにしたら改善できるのだろうか?
3つめの回答が、コンピュータ・システムにおける 信頼されたコード (trusted code) の量を減らすというものである。 コンピュータ・システムは、そのほとんどのコードを信頼されていないものとして 檻の中に閉じこめることができる。「信頼されていない」というのは、 これらの檻の中のコードは — 何をしようと、どんなに悪いふるまいをしようと、 どれほど多くのバグがあろうと — ユーザのセキュリティ要求を 侵犯できないという意味である。私たちはコンピュータ・システムの中にある 信頼されたコードの量を測定することができるし、 信頼されたコードの少ないシステムを作るようなプロセスを メタ・エンジニアリングすることができる。
もちろん、システム内のコード量を削減するための一般的な技法は 信頼されたコードを削減するのにも有用である。しかし、 さらなる技法を用いることで、信頼されたコードを 全コードに比べてずっと小さくすることができる。 これは信頼されたコードを削減すると同時にバグも減らすという 望ましい相乗効果をもたらす: 私たちは、信頼されたコードから バグをなくすためには、比較的高価な方法をとることができる。 なぜなら、これらのコードは量が少ないからである。 たとえば以下の例を考えよう。Sendmail のコードのうち、 メールのメッセージ・ヘッダから emailアドレスを抽出する部分がある。 [12] によれば、 2003年に Mark Dowd は、この部分にセキュリティ・ホールを発見している:
攻撃者はこの脆弱性をつかって、脆弱な Sendmail サーバ上で “root” あるいはスーパー・ユーザ権限をリモートから手にすることができます。 ... この脆弱性はきわめて危険です、なぜならこの攻撃は電子メールのメッセージ中に 含めることができ、攻撃する対象に関する何の特別な知識ももっていなくても 攻撃を成功させることができてしまうためです。 ... X-Force はこの脆弱性を利用して、実世界の状況で、 稼働中の Sendmail 環境に対して攻撃が可能であることを示しました。
ここで、このアドレス抽出コードが、次に示す単純な 2つの データフロー規則を強制するインタプリタの上で実行されたと仮定しよう:
このとき、このコードはユーザのセキュリティ要求を破ることはできない。 攻撃者がこのコードの完全な制御権を手にできるようなメッセージを送れば、 このコードが表示する電子メール・アドレスを変更することはできるだろう — しかし、それはべつにコードのバグを利用しなくともおこなえる。 攻撃者はこれ以外のどんな方法もできない。このアイデアの別な例については 5.2節を参照のこと。
(ここでは暗黙の仮定として、ユーザのセキュリティ要求が、メール作成時に そのメールから抜き出したアドレスの変更を禁じていないものとしている。 しかし実際の要求はもっと複雑かもしれない。 ユーザは複数のソースからもってきた添付ファイルをまとめて メールを書いているということもありうる。このとき、ユーザは それぞれの添付ファイルが別の添付ファイルやメールのヘッダ等に 影響することのないようにしたいかもしれない。このような場合、 インタプリタはこれに沿ったデータフロー規則を適用することが必要となる。)
信頼されたコードの量を充分に減らし、またバグの発生率も充分に減らすことができれば、 信頼された部分にバグが存在しないようなシステムが作成可能であるという期待を それなりに持つことができる。 おそらく、信頼されていない部分のコードにはまだバグがあるかもしれないが、 それらのバグはユーザのセキュリティ要求を侵犯するものではない。 セキュアなソフトウェア・システムを製作することは、 バグのないソフトウェア・システムを製作することよりも簡単なのである。
多くの人々にとって、“セキュリティ”というのは 現在の攻撃を観察し、その攻撃ができなくなるように何かを — どんなことでも! — 変えるということのようである。この手の対処療法を好む人々を理解するのは簡単だ。 以前なら成功していたかもしれない攻撃が失敗しているのを見れば、 それは防御する側につかの間の満足を与えるだろう。
ときに、これらの変更は攻撃によって利用されていた特定のバグを修正することによって おこなわれる。Linux オペレーティング・システムのひとつ、 Ubuntu ディストリビューションは、今年 100件以上の 緊急セキュリティ・パッチをリリースした。これらは プログラムの広範囲にわたるバグを修正するものである。 また、ときにこれらの変更はバグを修正しない。 “ファイヤウォール”や“アンチウイルス・システム”、および “侵入検知システム”といったものは、攻撃の対象となっている ソフトウェアにパッチをあてることなく、攻撃を認識するこころみである。
どちらにしても、これらの変更は、そもそものセキュリティ・ホールを 生みだす原因となったソフトウェア・エンジニアリング上の 欠陥については何も修正しない。もし私たちが すべての攻撃を止めるために改善することよりも、 昨日の攻撃を止めたことを成功とみなしているのであれば、 私たちのシステムが明日の攻撃に対して依然として脆弱なのを見ても 驚くにはあたらない。
多くの付加的な“セキュリティ”の取り組みは 『最小権限の原則』と呼ばれるものの実施である。 この原則は広く Saltzer と Schroeder の功績であるとされる。 彼らは [18] において次のように述べている: 『システム上のすべてのプログラム、およびすべてのユーザは、 その作業をするのに最低限必要な権限のみを用いて処理をおこなうべきである』
これらの“セキュリティ”上の取り組みは、次のようにしておこなわれる。 あるプログラム P が、オペレーティング・システムの資源 R にアクセスする 正当な理由が何も見つからないとする。このとき、オペレーティング・システムの 制限をかけて (おそらく制限をひろげて) P が R にアクセスできないようにする。 私たちは、画像表示プログラムがネットワーク経由で データを送信できないようにしたり、DNS検索プログラムが ディスクからデータを読み出せないようにしたり、といったことをする。 具体的な例については [3], [21], [11], [2], [15], [1], [16] および [23] を参照。 5.1節では qmail における例について述べる。
私は、この『最小権限の原則』は根本的に間違いだと確信するようになった。 権限を最小化すれば、いくつかのセキュリティ・ホールによる被害を 軽減できるかもしれないが、そもそもの穴をふさぐということはほとんどない。 権限の最小化は信頼されたコードの最小化と同じではないし、 信頼されたコードの最小化のような利点も得られない。そして、 これによって私たちがセキュアなコンピュータ・システムに多少なりとも 近づくわけではない。たとえば、[11] で挙げられている Netscape の “DNS helper”プログラムにおける制限について考えてみよう。 これは、このプログラムがローカルなディスクにアクセスするのを制限している。 この制限は、[8] にある libresolv のバグを防ぐことはせず、 それは Netscape のセキュリティ・ホールになる。攻撃者はこのバグを使って “DNS helper”の制御を得ることができ、以後 Netscape が使う すべての DNS データを変更してユーザの web接続を盗むことができる。 [11] 以前の状況では、“DNS helper”のバグはユーザのセキュリティ要求に 違反しており、修正する必要があった。しかし [11] 以後の状況でも、“DNS helper”のバグは依然として ユーザのセキュリティ要求に違反しており、修正する必要があった。
信頼されていないコードの決定的な特徴は、それがユーザのセキュリティ要求を 侵犯することはできないということである。“DNS helper”プログラムを 信頼されていないコードにするということは、必然的に、単にこれが アクセスするオペレーティング・システムの資源に制限を課す以上の 内部的な変更が必要になる。“DNS helper”は多くの情報源からくるデータを 扱っており、それぞれの情報源は他の情報源を変更できないように しなければならない。
プログラマは、自分たちのプログラムのうち、 さして重要でもない部分のスピードについて考え、 また心配するのにおそろしいほどの時間をかけている。 そして実際には、効率に対するこれらの企ては、 デバッグとメンテナンスのことを考えるとひどく ネガティブな影響を及ぼしている。些細な効率の アップについては、たとえば 97% ぐらいの時間なら、 忘れるべきなのである。 うかつな最適化は諸悪の根源である。― Knuth in [13, page 268]
実行速度を追求することのもっとも大きな影響は、 プログラマがコードのわずかな部分で時間を節約しようとして、 低水準なスピード・アップに労力を費やすことである。 プログラマは、これらの作業をやるときにそのことを意識している。
プログラマにとって、プログラミングの時間は増えるし、 バグの発生率も増える。一般的に、プログラマはこれらの 努力のほとんどすべてをスキップするように製作過程を変えることに まったく不満はない。Knuth のコメントは、プロファイリング・ツールの 使い方を知らない初心者に向けて書かれたもののようである。
不幸にも、速度の追求にはこれほど明白ではなく、 また直すことも簡単でないような副作用が存在する。 2.3節であげたアドレス抽出の例を思い出してほしい。 単純なデータフロー制限をかけるようなインタプリタを使って このアドレス抽出コードを書けば、このコードにバグが出ても セキュリティには関係ないようにできる。大きなメリットである。 しかし、ほとんどのプログラマは「インタプリタのコードなんて遅すぎる!」と言い、 それを試してみることもしないだろう。
オペレーティング・システム上の新しいプロセスを起動すれば、 インタプリタなしでも似たような制限をかけることができる。 詳細は 5.2節参照。しかし、ほとんどのプログラマは 「毎回のアドレス抽出のたびに新しいプロセスを起動するなんてできないよ!」と言い、 それを試してみることもしないだろう。
プログラミング言語や、プログラミング・アーキテクチャ、 システム・アーキテクチャなどを改善しようとする人ならだれでも、 同様のハードルを越える必要がある。これらの改良版を 使ってみた (あるいは使おうと考えた) プログラマのうちいくらかは、 確実にある状況における速度低下を体験する (あるいは想像する) ことだろう。 そしてこれらの改良版は「遅すぎる」と批判されることになる — マーケティング上の大失敗である。私は、自分のコンピュータの作業が終わるのを 待っていたくはない。誰か他人のコンピュータの作業が終わるのを 待っているのは実に苦痛である。私の研究の大部分は、 さまざまなレベルでシステムの性能を改善することに費やされている。 (たとえば、私の論文 [6] は “Curve25519: Diffie-Hellman の速度新記録” という題名になっている。) しかし私にとって、セキュリティは速度よりもはるかに重要だ。
私たちには脆弱でないソフトウェア・システムが必要である。 しかも、それは今すぐに必要なのである。たとえそれが 現在のシステムより 10倍遅いものであってもである。 明日からそれを速くするための作業を始めることができるだろう。
私は予測するが、ひとたび脆弱でないソフトウェア・システムが できてしまえば、セキュリティにはそんなに多くの CPU時間が 必要ではないことがわかると思う。CPU時間の大部分は、ごく一部の プログラムだけで消費されており、またそれらのプログラムのうち ごく一部のコードだけで消費されている。セキュリティ上の検証に 費やされる時間は、これらの『ホットスポット』の外では、 ほとんど気づかない程度のものである。典型的なホットスポットは 単一の情報源からのデータに対して数百万回の CPUサイクルの処理をおこなう。 現代のコンパイラ技術を使えば、場合によっては証明による補助も利用して、 セキュリティ上の検証すべてをこれらの内部ループの外におし出すことができるだろう。 ときどき現れる、やっかいなセキュリティ上の制約があるホットスポット、 たとえばネットワーク・パケットの暗号化などは、信頼されたバグのない コードにすることができる。
長年のあいだ、私は文献を参照したり、他の人のミスを分析したり、 また自分自身のミスを分析したりして、エラーを生みだしやすい プログラミング習慣というものを体系的に見つけだすようにしてきた。 そして、自分のプログラミング環境を設計しなおすことによって これらの習慣を排除しようとしてきた。
qmail を書いたころ、すでに私はこの方針で、 ある程度の改善ができていた。そして私は qmail 1.0 を開発する間に さらに改善することができた。だからといって私が qmail の 開発に使った環境のバグ発生率に満足しているというわけではない。 私のバグ発生率は過去10年のあいだ継続的に下がっており、 qmail のプログラミング環境の多くの部分は、もはや救いようのないほど 時代遅れのものになっている。
幸運にも、1990年代中盤の私のバグ発生率は十分に低かった — 第4節で述べるように、qmail のそれほど多くないコードでは、 qmail 1.0 に含まれるバグはほんの数個しかなかった。 これらのバグはどれもセキュリティ・ホールではない。 これが qmail の特出したセキュリティ記録の理由となっている。 しかし、qmail の権限最小化はまったく役に立たなかったことに注意してほしい。 このことについては 5.1節でさらに述べる。
この節では、アンチ-バグ・エンジニアリングの例をいくつかとり上げる。 これらはバグ発生率を減らすために、プログラミング言語を修正したり、 プログラムの構造を修正する、といったことである。
グローバル変数を使うことに対する、よくある反論は、 それが隠れたデータフローを作り、プログラマの予期せぬ結果が 生じるというものである。
たとえば qmail 0.74 で修正された次のバグを考えてほしい:
“newfield_datemake
は、もし
newfield_date
がすでに初期化されているときは、
それを放置してしまう。たとえ qmail-send
がバウンスするたびに
newfield_datemake
を呼び出すようになっていたとしても。”
私はもともと newfield_datemake
関数を
qmail-inject
プログラムの一部として書いていた。これは一通だけの
メッセージを送信するため、ひとつだけの Date フィールドを生成する必要があった。
この値は、便宜上 newfield_date
というグローバル変数に格納されていた。
しかし私は同じ関数を qmail-send
プログラムでも利用した。
これは多くの外向けのメッセージ (配送がされなかったことを通知する、
いわゆる『バウンス・メッセージ』) を送信し、各メッセージごとに
新しい Date フィールドを生成する必要があった。私は、
newfield_datemake
がすでに存在する
newfield_date
をリセットしないことを
忘れていた。そのため、このグローバル変数は、情報 —
つまり、古い Date フィールド — をひとつ前のメッセージから
次のメッセージへと伝達してしまうことになった。
また、隠れたデータフローはバッファ・オーバーフローのバグにおける
核心でもある。C における文 x[i] = m
は、
一見すると変数 x
のみを修正するかに見えるが
— もし i
が範囲外であれば —
これは実際にはプログラム中のあらゆる変数を変更する可能性がある、
関数の戻りアドレスやメモリ割り当て管理用の構造を含めてである。
同様に、x[i]
の値を読むことによりプログラム中に
ありとあらゆる変数を読む可能性がある。
qmail の設計におけるいくつかの性質により、qmail では 内部のデータフローがより容易に理解できるようになっている。 たとえば、qmail の多くの部分は個別の UNIX プロセスで実行される。 これらのプロセスはパイプラインやファイルシステム、 また時にはそれ以外の通信手段を用いて情報をやりとりするが、 お互いの変数に直接アクセスすることはできない。 各プロセスがもっている状態の数は比較的少ないため、 プログラマがデータフローをおかしくしてしまう機会も比較的少ない。 より低い水準では、私は配列にアクセスするためのさまざまな関数を 設計し、そこでは添え字の範囲が明らかに指定されていた。 そして私はそのチェックが難しいような関数は使用を避けていた。
いまでは私は、プログラミング言語による以下のようなサポートを よりいっそう主張するようになった: より小さなスケールでの分離、 健全な添え字検査、“サマリー”変数 (たとえば『この配列における 非ゼロ要素の数』など) の自動的な更新、などである。ここでいう 「健全な添え字検査」というのは、人々が通常知っているような、 範囲外であれば例外を出すといった「添え字検査」のことではない。 ここでいうのは、書き込み時には自動的に配列を拡張し、 読み込み時には自動的にゼロを補完するといったことである。 (メモリ不足だって? 4.2節を参照のこと) 同じことをわざわざ人手でやるのはナンセンスだ。
プログラマにとってのもうひとつの驚きは、
y = x + 1
を実行したときに、
y
が x
よりもずっと少なくなる場合があるということだ。
これは x
が表現可能な最大の整数、通常は
231 - 1 であるときに起こる。このとき y
は
表現可能な最小の整数、通常は -231 になる。
これまで qmail がもっともセキュリティ・ホールに近づいたのは、
(Georgi Guninski によって指摘された) 32ビット・カウンタの
潜在的なオーバーフローであった。これは私がチェックを
忘れていたものである。さいわいにも、カウンタの増加は
利用可能なメモリによって制限されており、
この利用可能なメモリも標準的な設定では制限されているものであった。
しかし別の文脈では、同様の 32ビットのインクリメント操作が
致命的なバグを引き起こしていた可能性もあるのである。
同様のコメントは、他の整数演算についてもあてはまる。 これらの操作のセマンティクスは、通常はプログラマの意図する 数学的なセマンティクスと一致しているが、ときにはずれている。 そのような事態を検出しようとすれば、オーバーフローを 検査するための余分な労力が必要になってしまう。 もしこういった事態でも整数が健全な数学のセマンティクスを 保持しようとするならば — 整数の範囲を拡張したり、 メモリを使い果たしたときのみ失敗するようにしたり — 巨大な整数ライブラリを使うための余分な労力が必要になってしまう。
ほとんどのプログラミング環境では、典型的なソフトウェアが
簡単に書けるようメタ・エンジニアリングされている。
むしろ、これらは不正なソフトウェアを書きにくくするように
メタ・エンジニアリングされるべきである。
私が普段必要とするものとは正確には一致しない操作は、
私が普段必要とするものと正確に一致する操作よりも、
記述により多くの手間がかかるようにすべきである。
私は 232 (あるいは 264) による算術的モジュロを
計算したいときがあるが、このようなとき私は追加の作業をおこなうことをいとわない。
言語によっては、 a + b
は正確にこのとおりの意味をもつ:
a
と b
の和である。このような言語はよく
一般的な使用には“遅すぎる”と批判されることがある。
以下のような内部ループ:
for (i = 0;i < n;++i) c[i] = a[i] + b[i];
が、突如 gmp_add()
のような
高精度の整数演算をおこなう関数を n
回呼び出すことになる。
しかし、コンピュータが大きな整数が使われている場所を覚えておき、
それらの場所では gmp_add()
を使うことにして、
通常の小さな整数が使われている場所では機械語の操作を
おこなうようにするのは、それほど大変ではない。
速度の低下を扱うのがより難しいケースもあるかもしれないが
— 2.6節で指摘したように — 私たちは
まず正しく動くコードを手にしてから、
つぎに速度について心配すべきなのである。
私は、コンピュータの世界におけるコマンドの インターフェイスには 2種類あると考えている: よいインターフェイスと、ユーザ・インターフェイスである。
ユーザ・インターフェイスの本質は構文解析 (parsing) にある : 通常、堅牢なエンジニアリングというよりもむしろ心理学的な要素によって定められた、 構造化されていないコマンド列を、構造化されたデータに変換するのである。
あるプログラマがユーザ・インターフェイスに対して指令するとき、 彼はクォート (quote) せねばならない: つまり、彼の構造化されたデータを 構造化されていないコマンド列に変換し、その解析器が おそらくそれを元の構造化されたデータに戻すであろうと期待するのである。
これは厄災へのステップである。構文解析器にはしばしばバグがある。 文書化されたインターフェイスに従っている ある種の入力を処理できないことがあるのだ。 クォートする側にもしばしばバグがある。これは正しい意味をもたない 出力を生成してしまう。ごくまれにしか起きない喜ばしい偶然により、 構文解析器とクォートする側とがインターフェイスを同じように誤解することがある。
これらのコメントは私が qmail の元々の文書でも述べたものであり、 そこでは私は 2つの例を示した。これは qmail の極めて単純化された内部ファイル構造と プログラム・レベルのインターフェイスが、構文解析とクォートをいかにして避けて 通ったかを説明している。しかし外的な制約のためにもっとましな インターフェイスを使うことが許されない場合に、いったいどのようにして 構文解析のバグとクォートのバグを避けられるのかについては、 私は言及しなかった。
たとえば qmail 0.74 で修正された、以下のバグを考えてみよう:
“qmail-inject
が USER
をクォートすべきかどうかチェックしない。”
ここで qmail-inject
がすることは、ユーザの名前をあらわす
From
行を作成することであった:
From: "D. J. Bernstein" <djb@cr.yp.to>
通常、ユーザのアカウント名 (この例では djb
) は
@
記号の前にそのまま挿入されるが、もしこの名前が
カッコなどの特殊な文字を含んでいる場合には、特定のやり方で
クォートする必要があった。私のテストでは USER
に特殊な文字が含まれている
ケースを検査しておらず、そのまま挿入する場合とエンコーディングして挿入する場合とを
区別していなかったのである。
べつの例として、UNIX の logger
システムにおける、
フォーマット文字列にまつわる次のバグを考えてみよう:
システムのロガーが次のことをしたとする:
本来は syslog(pri,"%s",buf) とすべきところを syslog(pri,buf)
...logger に対しては、これはおそらくサービス拒否攻撃以上の セキュリティ・ホールになることはないと思われる。 なぜなら攻撃者が到達可能な VM のアドレスを 印字可能な ASCII文字列に符号化するのには、 ひどい困難を伴うだろうからだ。しかし、逆アセンブルした コードを見ないことには確かなことはいえない。 万が一ということを考えたほうがよい。
syslog(pri,buf)
と syslog(pri,"%s",buf)
の
テストは、%
を buf
に入れても入れなくても、
結果は変わらないだろう。(この部分は [5] から引用した。
フォーマット文字列のバグが広く知られるようになる 4年前のものである。
[19, Introduction] も参照。)
「通常でない」入力に対して必要なアドホックなクォートをおこない、
「通常の」入力はそのまま写すというのは、ユーザ・インターフェイスの
普遍的な特徴である。このインターフェイスが改善できない状況で
バグを捉えるには、クォートの各ルールごとにシステマティックに
テストを分けることである。
以下のバグは qmail 0.90 で修正されたものである:
“.qmail-owner
が stat
できないのはエラーではなかった。”
Bob がいくつかのアドレスを ~bob/.qmail-buddies
に入れておくと、
qmail は bob-buddies
宛のメールをこれらのアドレスに転送する。
デフォルトでは、配送エラーは元の送信者に返されるが、
Bob が .qmail-buddies-owner
に異なるアドレスを入れておけば
エラーをこれらのアドレスに振り向けることができる。qmail は
.qmail-buddies-owner
が存在するかどうかを調べるのに
UNIX の stat()
関数を使っている。
stat()
がファイルは存在すると返してきたら、
qmail はエラーを bob-buddies-owner
に振り向ける。
stat()
がファイルは存在しないと返してきたら、
qmail はもとの送信者のアドレスを使う。
ここでの問題は、stat()
は一時的に
失敗することもあるということである。たとえば、
.qmail-buddies-owner
は一時的に不通になっている
ネットワーク・ファイルシステム上にあるかもしれない。
バージョン 0.90 以前の qmail は、このエラーを
ファイルが存在しないのと同様に扱っていた — もし実際には
ファイルが存在していれば、これは誤りである。
唯一の正しいふるまいは、あきらめて後でふたたび
トライするということである。私のテストには、
この一時的な失敗のケースは含まれていなかった。
私はいくつか一般的なケースでテストをし、
様々なエラーが発生したときの状況をつくりだす
やや手のこんだテストをおこなっていた。しかし、
ネットワーク・ファイルシステムのエラーを再現するような
テストはおこなっていなかったのである。
4.2節で述べるように、
エラー処理のためのコード量は
劇的に削減することができるだろうが、コンピュータ・システムには
つねになんらかのエラー処理コードがあるものである。
いかにして、ほとんど実行されないコードに含まれるバグを
防いだらよいのだろうか?
コードを以下のように分離していたら、テストはもっと簡単になっていただろう:
(1) 純粋に機能本位の、ファイルシステムだけでなく
どんなものにも対応できるプロトコル・ハンドラを作っておき、
(2) そのプロトコル・ハンドラに stat()
を処理するような
簡単なラッパを組み込んでおく。このプロトコル・ハンドラに対して
包括的なテストケースを提供するのはたやすいはずである —
また、このバグはすぐに明白になっていただろう。
ユーザ・インターフェイスを一般的にモデル化するには明白な 困難がつきまとう。バグ除去の研究については特にそうである。 その目標は、ユーザが、つまりこの場合はプログラマーが、 なるべく少ないミスで望む効果を得るということである。 この状況 — つまり人間心理 — をいかにして、実験することなしに モデル化できるだろうか? そもそも人間の助けなしに どうやって誤りを認識したらいいのだろう?
もし、ある種類の誤ちを認識するようなプログラムが書けたとしたら、 それはすばらしいことだ — 私たちはそれをユーザ・インターフェイスに 組み込み、それらの誤ちをなくすことができるだろう — しかし 依然として残りの誤ちを認識することはできない。数学者である私にとって、 この手の形式化がないことはイラつく。問題を定義することが できなければ、それを解くことはできない。さいわいにも、 研究というものはモデルがなくても進展できるし、また進展している。 私たちは人間が自分たちのプログラミングにおけるバグ率を 測定したものを観測できる。たとえ彼らの使っているアルゴリズムを 知らなくても、である。私たちは、たとえ数学的に証明できないとしても、 ある種のソフトウェア・エンジニアリング・ツールはバグを生みやすく、 またあるツールはそうではないことを知っている。
今日この日に至るまで、馬鹿なソフトウェア開発部の上司は 「プログラマの生産性」というものを「生産されたコードの行数」で 評価しています。実際には「消費されたコードの行数」のほうが ずっと適切であるにもかかわらず、です。― Dijkstra in [9, page EWD962-4]
この節ではコード量を最小化するためのメタ・エンジニアリングの いくつかの例を述べる: コード量を減らすために、 プログラミング言語やプログラムの構造を変える、といったことである。 第3節で見たように、これらの例のうちいくつかは qmail で使われており、 qmail のバグの少なさに貢献している。また、今より もっとうまくやれる可能性のある例も示す。
以下のコードは Sendmail (バージョン 8.8.5 における util.c
の
1924行目) からとってきたものである:
if (dup2(fdv[1], 1) < 0) { syserr("%s: cannot dup2 for stdout", argv[0]); _exit(EX_OSERR); } close(fdv[1]);
この dup2()
関数は、あるファイル記述子を
ひとつの番号から別の番号へとコピーする。
この dup2()
とclose()
のパターンは
ファイル記述子をある番号から別の番号へと移動させる。
Sendmail 内には他にも、これと基本的に同じ
dup2()
とclose()
のパターンがいくつか現れる。
この特定のパターンは、qmail では一回しか現れない。
1ダース近くの場所から呼ばれている fd_move()
関数の
中でだけである:
int fd_move(int to,int from) { if (to == from) return 0; if (fd_copy(to,from) == -1) return -1; close(from); return 0; }
ほとんどのプログラマは、これほど小さな関数をわざわざ作ることはしないだろう。
しかし、この dup2()/close()
のパターンが
fd_move()
で置き換えられるところでは、どこでも
数語が節約できる。こういった箇所を 1ダースほど置き換えれば、
この関数を書くことよりもはるかにコードの節約になるだろう
(また、この関数はテストの対象としてもふさわしい)。
同様の利益はより大きなシステムに対して、また多様な関数に対して適用できる。
fd_move()
はひとつの例にすぎない。共通した操作の列を
自動的にスキャンできれば、多くの場合、新しい補助関数を提案させることが
できるだろうが、たとえ自動化の助けがなくても、私はしょっちゅう
自分自身でこう考えているのである: 「これは前に見たことがなかったっけ?」
そして、既存のコードから新しい関数を作るようにしている。
qmail-local にある、以下の例を見てみよう:
if (!stralloc_cats(&dtline,"\n")) temp_nomem();
この関数 stralloc_cats
は、動的な大きさをもつ
文字列変数 dtline
の内容を、
これまでの内容にひとつの改行文字を加えたものに変更する。
しかし、この関数はメモリ不足を起こす可能性がある。
その場合 stralloc_cats
は 0
を返し、
qmail-local
は temp_nomem()
経由で終了する。
これは qmail の他のシステムに、あとでもう一度やりなおすように伝える。
qmail 中には何千もの条件分岐が存在するが、そのうちの半分近くは
— 正確にいくつかは数えていないが — 一時的なエラーかどうかを
チェックする以上のことはしていない。多くの場合、私は
以下のような関数を作って
void outs(s) char *s; { if (substdio_puts(&ss1,s) == -1) _exit(111); }
操作を実行させ、一時的なエラーが発生したらプログラムを終了するようにしてある。 しかし、各操作ごとに毎回同じ作業をくりかえすのは好きではなかったし、 いまでも好きではない。
これらのテストを、比較的小さな低水準のサブルーチン
(メモリ割り当てやディスク読み込みなど) に押しこめておき、
どんな一時的なエラーが発生してもプログラムが終了するように
作っておくことはできただろう。しかし、この戦略は
qmail-send
のように長時間走るプログラムに対しては使えない。
これらのプログラムは、システム管理者が指示するまでは
終了すべきではないからである! 多くのライブラリや言語が
同様の戦略をとっており、その結果、どれも長時間走るプログラムに対しては
使えなくなってしまっている。
もしかすると、特殊な動きをするプログラムは それ用に特化されたソフトウェア開発環境を使うべきだということなのかもしれない。 しかし、私は長時間走るというだけのプログラムを、特殊なプログラムとは考えない。 むしろ私はそういったプログラムを作るのに不適切な ソフトウェア開発環境を作ってしまうという風潮のほうを問題視している。
さいわいにも、プログラミング言語はより強力な 例外処理機構というものを持つことができるし、あるものは実際にそなえている。 これは明確に定義されたサブ・プログラムの実行を中止させ、 場合によっては自動的にエラー報告を処理してくれるものもある。 これらの言語でなら、私は次のように書くことができるだろう:
dtline += "\n"
あるいは単にこう書けるかもしれない:
エラー・チェックのための余分な労力の必要がないのである。 ここで削減されたコード量は、バグを減らすことだろう。 たとえば、以下のバグ “もしstralloc_cats(&dtline,"\n")
ipme_init()
が -1 を返した場合には、qmail-remote
は続行しなければならない”
(qmail 0.92 で修正) のようなものは起きなかったであろう、といえる。
qmail を書いたとき、エンドユーザがコンパイルして使うのが
C よりもずっと大変だという理由で、私は多くの言語を却下していた。
おかしなことに、私はもっとましな言語でコードを書いてから、
それを自動的な変換器を使って、配布言語としての C コードに
変換するという可能性にまったく気づいていなかった。
Stroustrup による元々のコンパイラ、C++ から C へと変換する cfront
は
インスピレーションを与える例であるが、私の知るかぎり
これに例外処理のサポートが入ることはなかった。
UNIX には、ネットワーク上の接続を listen する
汎用ツールである inetd
がある。ネットワーク接続がなされたとき、
inetd
はそれを処理するために別のプログラムを起動する。
たとえば inetd
は受けつけた SMTP 接続を処理するのに
qmail-smtpd
を走らせることができる。qmail-smtpd
プログラムが
ネットワークやマルチタスク等々について気にする必要はない。
これはひとつのクライアントから SMTP のコマンドを標準入力経由で
受けとり、そのレスポンスを標準出力に返すだけである。
Sendmail は、ネットワーク接続を listen するためのコードを
自分で持っている。このコードは inetd
よりも複雑である。
おもな理由は、これがシステムの負荷を監視して CPU に対する
大きな競合があるときにはサービスを削減するということをしているからである。
なぜ Sendmail は CPU が高負荷のときにメールを処理したがらないのだろうか?
基本的な問題は、Sendmail が新しいメッセージを受けとるやいなや、
これはそのメッセージを配送すべきかどうか決定し、
またそれを実際に配送するのに大きな労力をかけだすことである。
もし多くのメッセージが同時に届き、Sendmail がそれらすべてを
一度に配送しようとすると、通常これはメモリ不足になり、
ほとんどの配送に失敗することになるだろう。Sendmail は
このような状況をシステムの負荷を検査することで認識しようとしているのである。
CPU が高負荷の場合、Sendmail は新しいメッセージを 未配送メッセージのためのキューに入れておく。 Sendmail にはバックグラウンド配送のメカニズムがあり、 これは定期的にキューをチェックして、そこに入れられた メッセージを順番に配送していく。もし一度に多くのメッセージが キューに入ったら、それらのメッセージは次の順番がくるまで 気づかれることさえなく、その後かなり制限された並列処理によって 処理されることになる。
このキューが走査される間隔 queue-run は 「通常、30分から1時間のあいだに設定します」とドキュメントには 書かれている。「RFC 1123 のセクション 5.3.1.1 では、この値は 最低でも 30分にするよう推奨されています」 この値を非常に短く、 たとえば 30秒ほどに設定したシステム管理者は、Sendmail が キューに入ったメッセージを1日に何千回と配送しようとする光景を 見ることになる。
なぜ Sendmail はフォアグラウンドで配送しようとする
機構になっているのだろうか? なぜ受けとったすべてのメッセージを
キューに入れないのだろう? その答えは —
CPU が高負荷でないときでさえ、キューに入ったメッセージは
すぐには配送されないからである。受けとったすべてのメッセージを
キューに入れるということは、次にキューが走査されるまで
配送を待つということを意味する。ユーザは、ときに配送が遅れていると
不満をこぼすだろう。qmail では、メッセージがキューに
入ったときに、追加のコードがバックグラウンドの
配送メカニズムである qmail-send
に通知するようになっている。
qmail-send
プログラムは、(高負荷でないときは) すぐ配送を開始し、
必要ならば適切なスケジュールで次回またトライする。
したがって、フォアグラウンドで配送する機構の必要性はなくなる。
qmail には、フォアグラウンドで配送するメカニズムは存在しない。
さらに、システムの負荷をチェックしなければならないという
必要性もなくなる。qmail は inetd
からまともに走らせることができる。
もしシステムの負荷をチェックしたいと思ったら、私は
それを各アプリケーションのコード中で繰り返すよりも、
inetd
のような汎用ツールにやらせているだろう。
私が UNIX を、正確には Ultrix を、使いだしたのは 20年前である。
私は自分の .forward
を設定して、
/tmp
にファイルを作成するようなプログラムを
走らせるようにしたことを覚えている。その結果できた
何千個というファイルを調べているうち、私は Sendmail が、
ときに私自身のものとは異なる uid でこのプログラムを
走らせていることを発見して驚いたものだった。
Sendmail は、ユーザの .forward
を
以下のように処理している。まずこれは、ユーザがその
.forward
を読めるようになっているかどうかを判定する。
もしかすると、ユーザは別のユーザが所有している
秘密のファイルへのシンボリック・リンクを .forward
として
設定しているかもしれない。次にこれは .forward
から
配送の指示を読み込み、これを書きとめておく (おそらくは
後ほど使うキュー・ファイルの中に)。同時に、これを指示した
ユーザも覚えておく — プログラムを走らせるよう指示しているユーザは
とくにそうである。これはかなりのコード量になっており
(たとえば safefile.c
の全部と、
この情報をコピーするためにあちこち散らばっているコードなど)、
かなりのバグを有している。
もちろん、オペレーティング・システムは、
あるユーザがファイルを読むことができるかどうか判定するコードを
すでに持っており、そのユーザを管理するためのコードも
自分で持っている。なぜ同じコードをふたたび書くのだろうか?
qmail がユーザにメッセージを配送するときは、
単に適切な uid で配送プログラムの qmail-local
を走らせるだけである。
qmail-local
がユーザによる配送指示を読みこむとき、
オペレーティング・システムは自動的にそのユーザが
その指示を読む権限をもっているかどうかをチェックする。
qmail-local
が、ユーザの指定したプログラムを走らせるとき、
オペレーティング・システムは自動的にそのプログラムに
正しい uid を割り当てる。
このコードの再利用には、私は少量の CPU 時間の代償を支払っている。
qmail は各配送ごとに個別のプロセスを作成するのである。
しかし私はまた、Sendmail が権限をチェックするのに使う、
すべての追加システム・コールも使わずにすんだ。ともかく、
qmail の fork()
呼び出しが実際のメール
配送コンピュータのボトルネックになっているという実例がないかぎり、
私はこの時間を短縮するための余分なコードに時間を
費やす気にはならないだろう。
国家安全保障局 (NSA) にある SMTPサーバが、
efd-friends@nsa.gov
メーリング・リストに対するメールを
受け取ったとしよう。この MTA はどうやって、nsa.gov
に宛てた
メールを受けとるべきであると判断するのだろうか?
この MTA はどうやって efd-friends
に対する
配送方法の指示を得るのだろう?
この MTA は、おそらく nsa.gov
や efd-friends
といった名前を
データベースから検索することになる。もしかすると、このデータベースは
「データベース」という名前では呼ばれてはおらず、
「連想配列」かなにかかもしれないが、何であれこれは
システム管理者あるいはメーリング・リスト管理者が
efd-friends
のような名前のもとに格納しておいて、
なんらかの裏方の情報を引き出すことができるようになっている。
Sendmail では、nsa.gov
のような名前や、
その他のさまざまな設定項目は、非常に複雑な形式の
「設定ファイル」と呼ばれるものの中に羅列されている。
efd-friends
のようなメーリング・リストの一覧は
「aliasesファイル」の中に格納されており、これらのファイルから
nsa.gov
や efd-friends
といった名前を
探しだすためにはかなりの量の解析用コードを必要とする。
もちろん、オペレーティング・システムは、 データの塊を特定の名前のもとで格納するためのコードを すでに持っており、そのデータをあとから特定の名前で取り出すための コードも持っている。これらのデータの塊は「ファイル」と呼ばれており、 その名前は「ファイル名」と呼ばれている。そしてそのコードは 「ファイルシステム」というのである。 なぜ同じコードをふたたび書くのだろうか?
qmail では、efd-friends
に対する配送の指示は
.qmail-efd-friends
と名づけられたファイルの中に記されている。
これらの指示を取り出したり変更したりするのは、単にファイルを開くだけですむ。
ユーザはこれらのファイルを作成したり管理したりするためのツールを
すでに持っている。qmail がそれらのツールを再発明する必要はない。
同様に、私は nsa.gov
のための設定も
/var/qmail/control/domains/nsa.gov
ファイルに
書くようにしておき、同じくシンプルなコードを書くべきだった。
しかし私はこれよりもやや込み入ったコードが必要になるような決定をした。
nsa.gov
はディレクトリ中の1ファイルではなく、
ファイル中の1行なのである。私は、効率のことが気になっていた。
ほとんどの UNIX ファイルシステムはディレクトリにアクセスするのに
愚直な逐次的アルゴリズムを用いており、私は数千のドメインを
管理しているコンピュータ上で qmail が遅くなるのがいやだったのである。
また、ほとんどの UNIX ファイルシステムは小さなファイルを格納するのにも
数キロバイトというスペースを消費していた。
いまから考えると、こうしたコードを書いたのは愚かだったと思う。 このファイルの解析用コードだけではなく、メッセージを格納したファイルを ディレクトリ中に分散させるようにしたこともそうである。 純粋に想像上の性能の問題を、ろくにボトルネックとして測定もしないうちに 解決しようとしていた。また、たとえボトルネックとして測定されたとしても (実際、メッセージ・ファイルの件については活発なサイトでは 本当に問題となった)、この問題は根本から対策するべきであった。 つまり、そのファイルシステムに依存しているすべてのプログラムを 複雑化させるよりも、ファイルシステムそのものを直すべきだったのである。
「たとえこれらのプログラムがすべて侵入されて、
侵入者が qmaild
、qmails
および qmailr
のアカウントと
メールのキューを自由にコントロールできたとしても、
侵入者はまだシステムをのっとることはできません」
と私は qmail の文書に書いた。
「これ以外のプログラムで、これらの5つの結果を
信頼しているものはひとつもありません」
私は続けて、権限の最小化がセキュリティの
助けになるかのようなことを言っていた。
しばしの間、これについて考えてみよう。
仮に qmail-remote
にバグが存在し、攻撃者が
qmailr
アカウントをコントロールできてしまったとする。
この場合、たとえネットワーク接続が暗号によって強力に
守られているとしても、攻撃者はシステムの外向けに宛てたメールを
盗み見たり改竄したりすることができる。これはセキュリティ上の大惨事であり、
修正する必要がある。qmail がこの厄災を逃れる唯一の方法は
バグをなくすことだけである。
同様に、私は qmail の中で root所有のファイルをいじれる部分が いかに少ないかをことさら強調すべきではなかったと思う。 qmail のほとんどすべてのコードには、ディスク上あるいは メール・システム経由を経由して、通常のユーザが所有している ファイルを操作できる部分がある。したがって、これらのプログラムは ユーザのセキュリティ要求に違反できる立場にあるのだ。 このセキュリティ・ホールを逃れる唯一の方法は、 バグをなくすことだけである。
ワード・プロセッサや音楽プレイヤーを書いているプログラマは、 通常セキュリティについては考えない。しかしユーザは 電子メールで受け取ったり、ウェブからダウンロードした ファイルをこれらのプログラムで扱いたいと思うだろう。 これらのファイルのうちあるものは、攻撃者がこしらえたものである。 これらのプログラムは、こういった攻撃者に利用されてしまう バグをもっていることがよくある。
2004年に「UNIX セキュリティ・ホール」という授業を教えたとき、 私は宿題として、学生に新しいセキュリティ・ホールを発見するように言った。 そして、生徒が発見した 44個のセキュリティ・ホールを発表することになった。 これらのプログラムのほとんどは通常 — 間違って — システムの信頼されたコードの外にあるとみなされていたのである。
たとえば Ariel Berkman は動画再生ライブラリの xine-lib に バッファ・オーバーフローがあることを発見した。ここで、 ユーザはウェブからダウンロードした動画を見るたび リスクにさらされていることになる。つまり、もしその動画が 攻撃者によって準備されたものだったら、その攻撃者は ユーザのファイルを読んだり書き換えたり、ユーザが走らせている プログラムを監視したり、といったことができてしまうかもしれないのである。
「セキュア」なオペレーティング・システムや、 「バーチャル・マシン」などは、信頼されたコード量が少ないので セキュリティが徹底できると言われている。よくよく調べてみると、 不幸にも、これらの「セキュリティ」は、ひとつのプログラムが ひとつ以上の情報源からくるデータを扱いだしたとたんに 意味のないものになる。オペレーティング・システムは、 あるメール・プログラム中のメッセージが、同じプログラム中で 処理されている別のメッセージを盗み見たり改竄したりするのを まったく防げない。また、ブラウザ中のあるウェブページが、 同じブラウザ中で処理されている別のページを盗み見たり改竄したりするのも まったく防げない。実際には、メール・プログラムのコードも、 ブラウザのコードも、信頼されたコード群の一部なのである。 これらのコードに含まれるバグは、ユーザのセキュリティ要求に 違反できる立場にあるのである。
jpegtopnm
プログラムは圧縮された JPEG 画像ファイルを読みこむ。
これは画像を復元し、ビットマップ出力を生成して終了する。
いま現在、このプログラムは信頼されている。つまり、
このプログラムにおけるバグはセキュリティを侵犯する可能性がある。
これを解決するにはどうしたらよいだろうか。
この jpegtopnm
プログラムを「究極のサンドボックス」中で走らせることを
考えてみよう。これは、そのプログラムが JPEG ファイルを標準入力から
読み込む、ビットマップを標準出力に出力する、そして限られた量の
メモリを割り当てる以外のことは何もさせない。既存の UNIX ツールを使えば、
root がこういったサンドボックスを作ることはかなり簡単である:
RLIMIT_NOFILE
の現在値および最大値を 0 にすればできる。
chdir
、chroot
する。
setuid(targetuid)
、
kill(-1,SIGKILL)
、 および _exit(0)
を使うようにし、
子プロセスが正常終了したかどうかをチェックすればよい。
kill()
、ptrace()
等を禁止する。
これは、gid と uid を対象の uid に設定すればよい。
fork()
を禁止する。
これは RLIMIT_NPROC
の現在値および最大値を 0 にすればできる。
この時点で、オペレーティング・システムに深刻なバグがあるのでない限り、 このプログラムは最初に与えられたファイル記述子以外、 いっさいの通信チャンネルを持たなくなる。
ここで攻撃者が、jpegtopnm
のバグを悪用する凶悪な JPEG を送りつけて、
このプログラムを完全に支配できたとしよう。このとき攻撃者は、
自分が望むどんなビットマップも出力できる — しかしこれは別に
バグを悪用しなくとも可能なことなのである。攻撃者はこれ以外に何もできない。
この時点で、jpegtopnm
はもはや信頼されたコードの一部ではなくなる。
なぜなら jpegtopnm
におけるバグはもはやユーザのセキュリティ要求に対して
脅威とはなりえないからだ。
2.3節でみたように、 ここでのユーザのセキュリティ要求は、 JPEGファイルに影響を与えることのできる人間なら誰でも、 その結果生じるビットマップをコントロールしてよいと仮定している。 ここにはセキュリティ要求自体の限界もあるが、これは穏健なものであろう。 私は JPEG画像の各部分が、お互いに保護されるべきだという議論を 聞いたことがない。より重要なこととして、これらセキュリティ要求における多様性が、 コードをサンドボックスに分割することによる多様性によって 対応できないと考える理由は何もない。
私はまた、CPU があるプロセスから別のプロセスに秘密の情報を
漏らしてしまうことはないと仮定しているが、この仮定には議論の余地がある。
もしかすると CPU 命令に対するアクセスも制限する必要があるかもしれない。
たとえば2.3節でみたようなインタプリタの場合である。
性能における議論については 2.6節を参照のこと。
Ariel Berkman は私の助言をうけて、UNIXの標準的な
画像表示ツールである xloadimage
を再設計し、
プログラムのほとんどすべてをモジュール化して
jpegtopnm
のような一連のフィルタから構成した。
各フィルタは上で示した方法により簡単に閉じ込めることができ、
その結果、信頼されたコード量がずっと少なくてすむようになる。
ユーザのメール・ボックスにあるメールを表示するプログラムを 考えてみよう。単一情報源の処理 — たとえば、添付された JPEG ファイルを復元する、など — は、5.2節で示した方法で 閉じ込めることができる。しかし複数の情報源からのデータを 組み合わせるような処理はどうなのだろうか?
メッセージの件名一覧を表示するということは、複数のメッセージの 情報を組み合わせていることになる。各メッセージはリスト中の自分の エントリをコントロールすることが許されているが、 他のエントリに影響を与えることは許されていない。 たとえこの一覧が最初のうちはうまく生成できたとしても、 それ以降のリスト処理でバグがあった場合は、各エントリを 分離するという原則に違反する可能性が生じる。
もし個々のメッセージの件名が別々の場所に格納され、 独立して処理されたのちに統合されるとすれば、状況はまったく違ってくる。 各処理はいまや単一情報源の処理となり、あとから統合すれば 信頼されたコードの量を減らすことになる。
たとえばクロスサイト・スクリプティングを防ぐためには、 現在のところ、複数の情報源からくるデータをひとつのファイルにまとめる すべてのコードと、そのファイルをウェブページに変換するすべてのコードに対して 慎重な注意を払わなくてはならない。もし異なる情報源からくる データが分離されたままで、ブラウザによって最後に統合すれば いいだけだとしたら、注意を払うべき箇所は最終的な 統合部分のコードだけになる。
私は qmail のコードを信頼されていない檻の中に閉じ込めることは できていなかった。これらのコードにあるバグはどれでもセキュリティ・ホールに なりえたのである。 第3節および第4節で述べたように、 この失敗にもかかわらず qmail が生きのびた理由は、 バグが非常に少なかったためである。 このまま全体のコード量とバグ発生率を削減していけば、 もっと巨大なシステムでも同じような生き残りが可能かもしれない。 聞いたところでは、50万行にわたるコードにバグがないというシステムもあるようである。
私のノートパソコンには、これよりはるかに多くのコードが 搭載されており、はるかに多くのコードが私のセキュリティ要求を 破れる立場にある — しかし、これらのコードにあるバグすべてが 一掃された世界というものは想像可能である。
しかしながら、5.2節で示したように、 巨大なコードでも非常に低いコスト — とにかくバグをなくすよりは少ないコストで確実に — 信頼されたコード群から除去できる場合がある。 私はこの例のスケーラビリティについて楽観的な見通しをもっている。 信頼されたコードが、究極的にはどれくらい小さくなるのか、 私にはそれを正確に見積ることはできないが、それが明らかになるのを 私は心待ちにしている。もちろん、それでも残りのコードからは バグを除去する必要はあるのだが!
Date of this document: 2010.11.02;
translated by Yusuke Shinyama from 2007.11.01 version.
Permanent ID of this document: acd21e54d527dabf29afac0a2ae8ef41