大垣 靖男(おおがき やすお)
University of Denver卒。同校にてコンピュータ・サイエンスとビジネスを学ぶ。株式会社シーエーシーを経て,エレクトロニック・サービス・イニシアチブ有限会社を設立。オープンソース製品は比較的古くから利用し,Linuxは0.9xから利用している。オープンソース・システム開発への参加はエレクトロニック・サービス・イニシアチブ設立後から行い,複数のプロジェクトに参加している。

 このPart3では,Webアプリケーションのプログラミングでは必須のセキュリティ対策を解説します。

 Webアプリケーションといっても,セキュリティ対策の考え方がほかのコンピュータ・システムと大きく異なるわけではありません。しかし,「Webアプリケーションは最も危険なアプリケーションである」と認識する必要はあります。図1はセキュリティ関連情報のコミュニティであるSecurityFocusが発表した2006年5月のセキュリティ問題の統計です。セキュリティ問題のレポートの半数以上がWebアプリケーションに関するものになっています。「企業のWebアプリケーションには,ほぼ100%セキュリティ上の脆弱性が存在する」と指摘するセキュリティ専門家も少なくありません。

図1●セキュリティ問題の内訳(出典:SecurityFocus,2006年5月)
図1●セキュリティ問題の内訳(出典:SecurityFocus,2006年5月)

構築時に単純ミスを犯さない

 Webアプリケーション・プログラミングのセキュリティ対策は,単純ミスを犯さないことに尽きるといえます。

 Webアプリケーションの脆弱性の原因は,大きく「単純なミス」「外部要因」「情報不足」の三つに分けられます(表1)。

表1●Webアプリケーションの脆弱性の発生原因
表1●Webアプリケーションの脆弱性の発生原因

 このうち,外部要因については不特定多数に公開するWebシステムの場合,サービス提供側が対策を施すことはほとんど不可能です。例えば,ブラウザの脆弱性によってクライアント環境にキー・ロガー*1がインストールされ,IDが盗まれるというようなケースがあります。Webサイトの運営者がこれを防ぐことはできません。可能な対策は,ユーザーが最後にログインした日時,IPアドレス,ドメイン名をログイン時に表示することくらいです。

 情報不足も一朝一夕には解決できません。Webアプリケーションには比較的新しい技術/方法論が利用されるため,次々と新たなセキュリティの問題が発生します。安全なWebアプリケーションを構築するには,常に正しいセキュリティ情報を収集することが重要です。

 ただ,セキュリティ情報を定期的に収集していても,情報不足によるセキュリティ問題は発生します。すでに知られているセキュリティ問題であれば対処が可能ですが,未知の問題には対処のしようがないからです。実際に,米国のCERT(Computer Emergency Response Team)と米Microsoftが2000年2月にJavaScriptインジェクション(クロスサイト・スクリプティングが悪用する脆弱性)の危険性を指摘したときには,非常に多くのWebアプリケーションが脆弱であったことが判明しました。

「入力と出力」「設計」に問題が発生しやすい

 では,Webアプリケーション構築時に必要なセキュリティ対策を見ていきましょう。セキュアなWebアプリケーションの構築には二つの重要な要素があります。一つ目は「入力と出力」,二つ目は「設計」です。

 入力と出力は,コード・レベルでのセキュリティ対策の基本です。入力されるデータを適切にチェックし,出力するデータを出力先に合わせて適切に変換する処理が不可欠です。これは言語やアプリケーションを問いません。

 例えば,メモリー管理をプログラマ自身が行わなければならないC/C++言語で構築されたアプリケーションでは,入力チェックを怠ると,不正な入力によって簡単にバッファ・オーバーフローが起こります*2。バッファ・オーバーフローが起こると,メモリーに不正なプログラムを実行するコードを書き込まれ,意図しないプログラムが勝手に実行されてしまう場合があります。C/C++言語で構築されたアプリケーションの脆弱性の多くは,入力チェックのミスやチェック漏れによるバッファ・オーバーフローが原因です。表2は,この記事の執筆時期に報告されたバッファ・オーバーフロー関連の脆弱性の一部です。短い期間にたくさんのバッファ・オーバーフローが見つかっています。

表2●バッファ・オーバーフロー問題の一例。CVE番号は,米国のNISTが管理している脆弱性データベースでのID番号
表2●バッファ・オーバーフロー問題の一例。CVE番号は,米国の(NIST)が管理している脆弱性データベースでのID番号
[画像のクリックで拡大表示]

 現在は,メモリー管理が不要なJavaやスクリプト言語を用いてWebアプリケーションを構築する方法が主流です。このため入力チェックのコーディングに問題があっても,基盤となる言語やフレームワークに問題がない限り,バッファ・オーバーフローによる問題は発生しません。しかし,入力チェックを怠ると様々なセキュリティ上の問題が発生する可能性があります。

 もう一つの要素である設計は,セキュリティの要である認証や権限管理の問題に直結します。このため,入出力のコーディング時に注意するだけでは防ぎきれない問題の原因になります。数年前までは,ユーザー名とパスワードをそのままCookieに保存して認証していたプログラムすらありました。こうした設計は論外です。現在では,Webアプリケーション・フレームワークの普及や,構築ノウハウの蓄積が進んできたことにより,設計に起因するセキュリティ問題は減少傾向にあります。ただ,設計の問題がなくなったわけではありません。現在でも,機密情報の保護を“行っているつもり”で対策になっていないアプリケーションは多々あります。

 Webアプリケーションのセキュリティ問題として代表的なJavaScriptインジェクションとSQLインジェクション(詳細は後述)の事例を検証すると,入力・出力処理と設計の両方に問題があった結果としてセキュリティ問題となってしまったものがほとんどです。

 以下では,「入力と出力」と「設計」に分けて,それぞれの問題と対処法を解説します。サンプルは主にPHPとJavaScriptで示しますが,基本的な考え方はどの言語でも同じです。

【入力と出力】指針にのっとって注意深いコーディングを

 入出力の処理では,まず外部システムからの入力を一切信用しないことが重要です。ユーザーがキーボードから入力したデータ,送られてくるメール,データベースなどのデータ,ストレージはいずれも信用できません。Webアプリケーションには,外部からの入力値であるにもかかわらず,一見するとプログラム内部で定義したように見える変数もあるので,こうしたものにも注意する必要があります。

 そして,ブラウザやデータベースなどの外部システムに出力するときには,そのシステムに合った正しい形式で出力しなければなりません。

 具体的にどのようなコーディングが必要かは,その対象によっても,言語によっても異なります。まずは全体的なコーディングの指針を四つ紹介します。

■コーディングの指針1 エラーを厳格に処理する

 Webアプリケーションでは,エラー・チェックを厳格に行う必要があります。通常処理では発生しないエラーは,すべて致命的なエラーとして処理しなければなりません。つまり,通常の処理を行っている場合にはエラーや例外が一切発生しないようなコーディングが必要です。

 通常のデスクトップ・アプリケーションなら,ある程度のエラーを想定したエラー処理を作り込み,異常終了を極力避けるほうがよい場合があるかもしれません。しかし,Webアプリケーションは攻撃可能な個所が非常に多く,それらに個別の対処を施すことは困難です。予期しない攻撃が成功しないようにするために,例外やエラーが発生した場合には必ず実行を停止するようにコーディングします。そうすることで,セキュリティ上の問題がプログラムに残ってしまっても,攻撃を受けるほどの大事に至らずに済む場合が多くあります。

 Webアプリケーションはリクエスト-レスポンス型のアプリケーションであるため,適切なエラー・ページを送信できれば処理を打ち切っても問題ありません。PHPのようにスクリプトの終了時に自動的にリソースをクローズする処理系ならば,単にエラー・ページを送信するだけで済みます*3

 Webアプリケーション構築によく使われるスクリプト系言語の多くは,変数を宣言せずに使ってもエラーにならないなど,エラーに対して寛容な仕様になっています。設定などで,より厳しいエラー・チェックが利用できる場合は利用すべきです。

 また,エラーが発生したときにはその内容を確実に記録すべきです。例えば,リスト1ではidが空だったときに,ただアプリケーションを終了させ,エラーの記録を残していません。このように,エラーを記録しないでいると,アプリケーションのバグ発見が遅れる原因になります。

<?php
//このページにはidが必須
if (empty($_GET['id'])) {
   // スクリプト実行を停止
   die('無効なリクエストです');
             // エラーを記録していない
}
   …
リスト1●エラーを記録せずに終了してしまう例

■コーディングの指針2 入力を完全にチェックする

 あらゆる入力はチェックが必要です。入力が,明示的に許可されている文字種,形式,長さ,範囲に収まっているかどうかをチェックします(図2)。

図2●入力を完全にチェックする方法。想定される形式であることを厳格にチェックする
図2●入力を完全にチェックする方法。想定される形式であることを厳格にチェックする

 セキュリティ対策として入力の「サニタイズ(sanitize)」という処理が紹介されることがありますが,これはお勧めできません。サニタイズとは「入力をチェックし,不正な文字や文字列を取り除いてプログラムで安全に処理できるようにすること」を意味します。サニタイズ処理として紹介される例の多くは,入力に問題があった場合に,問題を起こさないようなデータに変換して処理を続けるコードになっています。そのような処理は,「エラーを厳格に処理する」という指針1に反します。

 入力をチェックする際には正規表現を利用することが多いと思いますが,正規表現に不慣れなプログラマは,間違った正規表現でチェックしようとしてしまう場合があります。例えばリスト2(a)は,入力が整数であるかどうかをチェックするつもりで書かれたPHPコードの一部です。preg_match関数の第1引数の正規表現は,文字列の先頭(^)と末尾($)の設定がないので,数字を含む文字列なら何でもマッチしたと判定してしまいます。

リスト2●間違った正規表現を使うとチェック漏れが起こる
リスト2●間違った正規表現を使うとチェック漏れが起こる

 また,使用する正規表現関数がバイナリ・データを正しく処理できる「バイナリ・セーフ」な関数であるかどうかも重要です。(b)もまた整数であるかどうかをチェックしようとしています。(a)とは異なり,文字列の先頭と末尾を設定しているので正しく判定できるように見えます。しかし,使用しているereg関数がバイナリ・セーフな関数ではないため,正しいチェックができません。例えば,$_GET['id']に

1234\n 1; DROP TABLE product; --

という文字列を設定されると,改行文字(¥n)の手前の“1234”だけを見てマッチしたと判定してしまいます。数値であるかどうかを完全にチェックするには,例えばバイナリ・セーフなmb_ereg関数を用いて(c)のように記述します。if文の前にあるmb_regex_set_option('m')は複数行にわたる文字列にマッチさせるオプションで,改行文字が現れても判定を続けることを指示しています。

■コーディングの指針3 可能な限り出力をエスケープする

 外部システムへの出力は,エスケープ処理*4してはならない値を除き,すべてエスケープすべきです。外部システムとは,プログラム以外のシステムすべてです。具体的にはXML,HTMLの出力,データベースへのクエリーなどが外部システムへの出力です。

 例えば,HTMLの出力は,整数型のデータであっても,文字列としてエスケープ処理してからブラウザに出力します*5。ID番号のように正常な値が整数であるような変数でも,先の指針2の例のように入力チェックにミスがあると文字列が入り込む可能性があるためです。

 どうしてもエスケープできない変数のエスケープ処理だけを省略するようにコーディングしてあれば,安全性を確保できているかどうかの確認が容易になります。入力チェックをくぐりぬけて入り込んでくる不正なデータによるセカンドオーダー・インジェクション攻撃(後述)からも防御できます。

■コーディングの指針4 対策を多重化する

 セキュリティ対策には無駄が必要です。ハードウエアのセキリュティ対策では,RAID,2重化電源,無停電電源装置(UPS)など,通常の動作には不必要なハードウエアを追加して安全性を確保しています。ソフトウエアのセキュリティ対策にも,これと同じような無駄が必要です。

 一般的に,プログラマは効率的なプログラムを作るようなトレーニングを積んでいます。無駄な処理,重複した処理を行うコードは悪いコードだと考えている方も多いと思います。しかし,一つのミスがセキュリティ・ホールを生むようなコードは「十分に安全性を確保している」とは言いがたいです。プログラムの要所要所でフェール・セーフ的な対策を行ってセキュリティ対策を多重化してあれば,ミスが一つあってもセキュリティ・ホールにならずに済む場合が多くあります。数多くのWebアプリケーションにセキュリティ・ホールが見つかっている現状では,多重のセキュリティ対策は「不必要な無駄」ではなく「必須の対策」だと,とらえるべきです。