2012年4月16日月曜日

情報処理試験問題に学ぶJavaScriptのXSS対策

平成24年度春期の情報処理技術者試験の問題と解答(一部)が公開されていますね。情報セキュリティスペシャリスト試験(SC)の午後Ⅰ(全4問中2問を選択)では、問1と問2がWebアプリケーションに関する問題でした。このエントリでは問1について書きます。

問1は、インターネット通販A社のサイトで脆弱性検査を実施したところ、指摘事項2点が報告されたところから始まります。

指摘事項A:画面の遷移の中で、暗号化通信と非暗号化通信が混在しているが、暗号化通信でだけ使用されるべきクッキーに、(   a   )属性が設定されていないページが存在する。
指摘事項B:任意のスクリプトが実行可能であるページが存在する。

指摘事項A中の(a)は、他を見なくても「セキュア」属性だと分かりますね。徳丸本(体系的に学ぶ 安全なWebアプリケーションの作り方)では、4.8.2クッキーのセキュア属性不備(P209)に説明があります。

指摘事項Bは、ここだけ読むと、XSSのようでもあり、サーバーサイドのスクリプトインジェクションのようでもありますが、検査ログからXSSであることがわかります(下図はIPAからの引用)。XSSは、徳丸本4.3.1クロスサイトスクリプティング(基本編)と4.3.2クロスサイトスクリプティング(発展編)にて説明しています。


ここまでは、ごく基本的な問題ですが、問題文P6に出てくる以下の部分は、少しだけひねってますね。
このプログラムは、利用者が入力した文字列をダイアログに表示するために、受け取ったパラメタの値をスクリプトに埋め込み、動的にスクリプトを生成する。図4の(   c    )行目では、通常のHTMLにパラメタの値を埋め込むときと同じ方法でエスケープ処理を行っていたことから、生成されるスクリプトに問題が生じてしまうことが分かった。
JavaScriptの文字列リテラルにパラメータを埋め込んでいる箇所を探すと、37行目であることが分かります。
37:  out.println("<a name=\"#\" onclick= \"alert('" + escape(word) + "')\">");
これは、徳丸本では、◆イベントハンドラのXSS(P109)に説明がある内容です。
イベントハンドラのJavaScript文字列リテラルの中味を動的生成する場合は、徳丸本P110に説明するように、二段階でエスケープしなければなりません。

①まず、データをJavaScript文字列リテラルとしてエスケープする
②この結果HTMLエスケープする

情報処理試験の問題文もそうなっていますが、問題はエスケープする文字です。徳丸本では、最低限エスケープが必要な文字として以下の4種を挙げています。


これに対して、情報処理試験の問題文では、エスケープすべき文字を2種類に限定しています。従って、上記4文字から2文字を除外する必要があります。
まず除外できるのがダブルクォート「"」です。JavaScriptの文字列リテラルは、シングルクォートでもダブルクォートでも囲むことができるので、両方の場合に対応する前提では、両方の引用符をエスケープする必要があります。しかし、この問題では、シングルクォートで文字列を囲っていることが分かっているので、ダブルクォートの方は、エスケープ対象から除外することができます。
残り3文字のうち、シングルクォート「'」とバックスラッシュ「\」はエスケープしないとXSS脆弱性となるので、この2文字が解答でしょうね。

なお、バックスラッシュのエスケープを怠ると、以下の文字列で攻撃が可能です。
\');alert("XSS");//
この場合、シングルクォートのみがエスケープされると、生成されるJavaScriptは下記となります。
alert('\\');alert("XSS");//')
攻撃文字列先頭の「\」と、シングルクォートのエスケープの「\」があわせて「\」一文字と解釈され、後続のシングルクォートがエスケープされない状態で余ります。このため、文字列リテラルが終端され、その後ろはJavaScriptのコマンドとして解釈されます。すなわち、スクリプトのインジェクションができたことになります。

一方、改行については、エスケープしなくても上記のような攻撃には至りませんが、JavaScriptのエラーにはなります。また、JavaScriptの文字列リテラルの引用符をシングルクォートで統一しているプロジェクトはマレだと思われるので、徳丸本の基準に従うのが実務上はよいと思います。あくまで、テストとして、エスケープの原理を理解していることを問う問題と考えるべきでしょう。

twitter等でたまに「情報処理試験対策のために徳丸本読んでる」等のツイートを見かけることがありますが、私は「試験対策になるのだろうか」と思っておりました。この問題を見る限り、情報セキュリティスペシャリスト試験(SC)の対策になる場合もありそうですね。但し、今回引用していませんが、実際に攻撃があったかどうかをログから判定する問題など、徳丸本の知識だけでは解答出来ないものも当然あります。

追記(2012/04/30)

水無月ばけらさんから指摘がありました。「平成24年度春期情報セキュリティスペシャリスト試験のXSS問題
  • 「このコードに限定した話」という仮定を置かない場合、escape2は単引用符、バックスラッシュ、改行に加え、二重引用符などもエスケープする必要があるかもしれない。
  • 「このコードに限定した話」と仮定し、かつ「不具合なく動作する」ことを想定した場合、escape2は単引用符、バックスラッシュ、改行をエスケープする必要がある。
  • 「このコードに限定した話」かつ「攻撃さえ防げれば良く、不具合があっても良い」と想定した場合、escape2は少なくとも単引用符をエスケープする必要がある。
    • 単引用符を「\'」にエスケープすれば、バックスラッシュのエスケープも必須になる
    • 単引用符を「\u0027」にエスケープすれば、バックスラッシュのエスケープは必要ないかもしれない (見落としがある? 突破できる?)
たしかにそうですね。
ということは、上記に指摘した解以外に、複数の解が正答になってしまうということで、問題としてはよろしくない、ということになります。この問題については全員を正解とするか、理論的に正答と見なせる解(何種類かありそうですが)を全て正答にする、という対処が必要になりそうです。IPAとしてはどうするのでしょうか。

追記(2012/06/08)

IPAから正答が公開されました。これを読むと、設問3(2)はやはりシングルクォートとバックスラッシュのエスケープを想定正解としていますね。\u0027にエスケープするとした人はいなかったのでしょうか。

[PR]
WAF始めました。詳しくはHASHコンサルティング株式会社まで。
安全なWebアプリケーションの作り方DRMフリーのPDFによる電子版もあります。

2012年4月11日水曜日

「クロスサイトスクリプティング対策」でGoogle検索して上位15記事を検証した

昨年の11月にブログエントリ『「SQLインジェクション対策」でGoogle検索して上位15記事を検証した』という記事を書いたところ、非常に好評で、「次はXSSについて書いてください」という要望をいただいておりました。中々XSSについては手がついておりませんでしたが、ようやく書いてみました。以下のURLで検索した結果の上位15位の記事を検証しました。
検索結果は変動するため、私が検索した際の結果をEvernoteの公開ノートとして記録しています。
記事の「正しさ」の検証基準としては、IPAの「安全なウェブサイトの作り方改訂第5版」を参考に、最低限として以下が記述されているかどうかを確認しました。
  • HTMLのエスケープ処理を行う
  • 属性値はダブルクォートで囲む
  • レスポンスヘッダのContent-Typeフィールドに文字コード(charset)の指定を行う
それでは始めます。

1位 クロスサイトスクリプティング対策の基本(前編)

Webアプリケーションセキュリティの大御所、国分裕さんの記事です。2002年11月なので約10年前の記事ですね。当時としては素晴らしい内容ですが、サニタイジングという用語の使い方など、さすがに現在初心者が読むにはふさわしくないでしょう。また、レスポンスヘッダに文字コード(文字エンコーディング)を指定するという説明はありません。
さらに、この記事では、現在ではHTTPヘッダインジェクション、あるいはHTTPレスポンス分割などと呼ばれる攻撃手法もXSSの一種として説明しています。当時は、HTTPヘッダインジェクションの解説はほとんどなかったので貴重な内容だったと思いますが、現在はXSSとHTTPヘッダインジェクションは別の脆弱性として分類することが一般的だと思います。

2位 クロスサイトスクリプティング

Wikipediaの解説です。
概ね正しい内容ですが、辞書的な書き方で、初心者が参考にするには読みにくい内容です。ここでも、文字エンコーディングについての言及はありません。

3位 第5回 クロスサイト・スクリプティング対策も忘れずに

徳丸の記事です。
この記事は、SQLインジェクションの改ざん対策の補助的な対策として書かれた内容ですので、XSS対策単体として読むと不十分な内容です。

4位 [1-2.]クロスサイトスクリプティング

セキュアプログラミング講座(旧版)の記事です。旧版の方が検索結果の上位にくるのですね。
内容は概ね正確ですが、「サニタイジング」という用語の使い方など、古さを感じます。さらに、文字エンコーディングの指定についても言及がありません。
古い記事を敢えて読む理由はないでしょう。

5位 クロスサイトスクリプティングの対策について|OKWave

OKWaveでのQ&Aの記事です。2002年の内容で、回答者は国分さんの記事(#1)を参照しています。この例に限らず、質問サイトのやりとは検索結果の上位にくることが多いようですが、必ずしも正確な回答でないので、参照する人は注意が必要です。
この記事も、現時点で参考にする必要はないでしょう。

6位 第8回 クロスサイトスクリプティング対策の落とし穴

大垣さんの記事です。「落とし穴」とあるように、基本的な対策を説明したものではありません。
また、細部を確認してみると、ミスが目立ちます。

(1)ereg_replaceの使い方の間違い
ereg_replaceの引数の順番が違います。

誤: $safe_text = ereg_replace($_GET['text'], '[<>]', '');
正: $safe_text = ereg_replace('[<>]', '', $_GET['text']);

(2)悪い例の選択が良くない
「しかし,javascript文字列がなくてもブラウザがJavaScriptと認識してしまうケースは多数あります。」と指摘した上で、「javascript以外でJavaScriptと認識する文字列」を説明していますが、元の例がonmouseoverイベントでの説明ですので、javascript:はそもそも不要です(後の方で説明されています)。javascript:スキームの例を示すのであれば、onmouseoverイベントではなく、a要素のhref属性に突っ込む例を示すべきでしょう。

(3)typo
また、以下のonmouseorverはonmouseoverのtypoでしょう。
<b onmouseorver="alert('XSS')">XSS</b>
また、読者は、「どうやってb要素にonmouseover属性を突っ込む攻撃ができるんだろう?」と悩んでしまいそうです。通常あり得ないシナリオで説明すると、余計なところで読者に負担を掛けます。

ということで、元々初心者向きでないことと、記事のクォリティが低いという問題があります。

7位 PHP と Web アプリケーションのセキュリティについてのメモ

小邨孝明さんの記事です。小邨さんは徳丸本のレビュアーのお一人で、同書執筆時にずいぶんとお世話になりました。
この記事は、PHPアプリケーションのセキュリティに関わるものにとっては必読です。特に、PHP固有の問題については、他の記事からは得られない貴重な内容が書かれています。XSSについても必要な内容がコンパクトにまとめられています。
また、小邨さんのブログもPHPのセキュリティの重要な情報源です。関係者は過去記事を一通り読んでおきましょう。

8位 配列データを一気にクロスサイトスクリプティング対策する関数(php)

入力データをあらかじめHTMLエスケープしてしまう関数が紹介されていますが、このような方法は現在では否定されています。
その理由は、プログラムの内部でHTMLエスケープ済みのデータを扱うことになり、生データが必要な場合に、一々アンエスケープしなければならず、却って煩雑になるからです。結果として、多重エスケープの問題が生じたり、エスケープもれ(すなわちXSS)が生じる原因になります。

しかしながら、PHPではこのような好ましくない書き方がかなり普及してしまっているような気がします。私がそう思う根拠は、PHPには、htmlspecialchars関数の第4引数(double_encode: 二重エスケープをしない指定)やhtmlspecialchars_ decode関数(アンエスケープ用関数)が用意されていることです。表示の際にHTMLエスケープするという標準的な書き方をする限り、これらの機能は必要ありませんし、PHP以外の言語では見かけない機能です。

あらかじめHTMLエスケープしておくという書き方は、PHPのマジッククオートと似た考え方と言えます。ご存じの通り、マジッククォートとは主にSQLインジェクション対策として、あらかじめ入力値のシングルクォートやバックスラッシュをエスケープしておくものですが、SQLインジェクション対策としては不完全である上に、多重エスケープなど副作用が大きく、PHP5.4では廃止されました。

ということで、無精をせずに、必要な都度HTMLエスケープするようにしましょう。あるいは、表示の際に自動的にHTMLエスケープしてくれるフレームワークを使いましょう。

9位 クロスサイトスクリプティング対策としてやるべき5つのこと

いかにもなタイトルですが、「5つのこと」として以下が推奨されいます。
  1. <>"&は文字参照にする
  2. Javascriptに動的に文字列を渡す時はURLエンコードする
  3. hrefやsrcの値がURLか確認する
  4. php.iniでsession.cookie_httponly=on
  5. httpd.confでTraceEnable Off
「属性値をダブルクォートで囲む」ことと「HTTPレスポンスヘッダに文字エンコーディングを指定する」が抜けています。とくに前者は致命的です。
また、上記2の「URLエンコード」という見出しは間違いのように見えますが、本文で説明されている内容はURLエンコード(%xxという形式)ではなく、Unicodeエスケープ(\uXXXXという形式)です。すなわち本文の方は正しいのですが、見出しでの用語の間違いはまずいです。

ということで、不正確な内容です。

10位 クロスサイトスクリプティングとは【XSS】 - 意味/解説/説明/定義 : IT用語辞典

用語辞典の解説です。
対策としては、訪問者からの入力内容をそのまま表示せずに、スクリプトなどのコードを識別して無効化する処理を施すことが必要である。
これはダメですね。「入力内容」、「スクリプトなどのコードを識別」は余計であり、誤解を招きます。

11位 クロスサイト スクリプティング フィルター

IEのXSSフィルタの説明であり、アプリケーション側の対策の話ではありません。

12位 クロスサイトスクリプティングとは

対策としては以下の3つが挙げられています。
  • 入力データのチェック
  • クッキーに重要な情報を保存しない
  • デフォルトでエスケープするテンプレートを使う
肝心のHTMLエスケープの説明がないので、XSS対策の説明としては不十分です。

13位 間違ったクロスサイトスクリプティング(XSS)対策 - YouTube

LACの川口洋さんによるビデオ解説です。1分程の短いプレゼンです。特別目新しい内容ではありませんが、面白く見ることができます。
あくまで、間違った例に対する説明であり、対する正しい方法が説明されているわけではありません。

14位 そのやり方でクロスサイトスクリプティング対策は完全ですか?

WAFの宣伝です。以下のように、まずXSS対策のプログラミングの難しさが説明されます。
クロスサイトスクリプティングは【略】一般的には「プログラマが、入力フォームを作る時にJavaScriptなどのコードを別な文字に置換するなどの対応を行うべき」と考えられています。
しかしながら、掲示板のような簡単なプログラムならいざ知らず、昨今のWebシステムは数百、数千のプログラムから構成されることが普通ですから、こうしたシステムで 全てをプログラマの努力に頼ることは難しいだろうという認識も生まれています。
これはまぁいい。「難しい」のは事実ですから。しかし、以下はどうでしょうか。
リリース後のアプリケーションにたった1箇所の問題があっただけで、それまでの検査コストがすべて無駄になる
「すべて無駄になる」はあんまりでしょう。
そこで、視点を変えてみましょう。もし、アプリケーションに対する攻撃をネットワークで防ぐことができたらどうでしょうか?
  • アプリケーションの追加リリースの度に検査する必要がなくなりますので、将来の追加コストを抑えることができます
  • 脆弱性検査などでも発見できない、未知の攻撃も防御することができます
  • セキュリティ試験などの工数を削減でき、システム開発にかかる費用の削減が期待できます
そんな理想的な話があるのでしょうか?

実は、Citrix NetScalerを使えばWebアプリケーションに対する攻撃をネットワークレベルで防ぐことができるのです!
これは酷い。何が酷いかというと、プログラマのプログラミングレベルの対処や脆弱性検査が「すべて無駄」で、WAFを導入すればWebアプリケーションに対する攻撃を防げるとしていること、「WAFを導入しさえすれば全てOK」と明示的には書いていないものの、読者がそういう印象を持つように誘導していることです。
実際には、WAFが全ての攻撃を防げるわけではなく、あくまで保険的対策です。上記の記事では、主・客が逆になるような、間違った印象を読者に与えてしまいます。

15位 CWE-79

CWE(Common Weakness Enumeration)の翻訳記事からXSSの解説です。CWEについては、IPAの解説記事から引用します。
共通脆弱性タイプ一覧CWE(Common Weakness Enumeration)は、ソフトウェアにおけるセキュリティ上の弱点(脆弱性)の種類を識別するための共通の基準を目指しています。
1999年頃から米国政府の支援を受けた非営利団体のMITREが中心となり仕様策定が行われ、2006年3月に最初の原案が公開されました。その後、40を超えるベンダーや研究機関が協力して仕様改善や内容拡充が行われ、2008年9月9日にCWEバージョン1.0が公開されました。
CWEでは、SQLインジェクション、クロスサイト・スクリプティング、バッファオーバーフローなど、多種多様にわたるソフトウェアの脆弱性を識別するための、脆弱性の種類(脆弱性タイプ)の一覧を体系化して提供しています。 
CWEは、脆弱性のハンドリングをする専門家は知っておく必要があるでしょうが、一般のWeb開発者は知らなくていいんじゃないかと思います。
XSSの説明ですが、なんか仰々しい感じで小難しいですね。例えば、以下のような箇所。
ソフトウェアにおいて信頼できない入力を受け付ける箇所を全て把握してください。
なんか、一昔前のセキュアプログラミングの教科書風です。XSS対策をする上では、入力に着目するのではなく、表示の箇所で淡々とエスケープ処理をすればよいわけで、入力の信頼できる・出来ないを分類する必要などありません。そんなことをすると、かえって対処もれ、ひいては脆弱性の原因になります。表示箇所で全てエスケープと考えた方が漏れがありません。

とりあえずのまとめ

「クロスサイトスクリプティング対策」でGoogle検索して上位15記事を検証しました。SQLインジェクションの時以上に、検索上位には良い記事が少ない状況です。上位15位の中で、開発者が読むべきXSS対策の記事は小邨さんの解説くらいでしょう。
脆弱性対処の方法をGoogleなどの検索サイトに頼るのは、賢明とは言えないというのが私の感想です。

XSS対策はどうすればよいか

それでは、XSS対策についてはどうすればよいでしょうか。
あらためて思いましたが、IPAの「安全なウェブサイトの作りかた」が脆弱性対処の規範と言えます。全てのWebアプリケーション開発者が安全なウェブサイトの作り方を学ぶべきだ思います。同書のチェックリストから、XSSの項目を引用します。

脆弱性の種類 対策の性質 実施項目
HTMLテキストの入力を許可しない場合の対策 根本的解決 ウェブページに出力する全ての要素に対して、エスケープ処理を施す。
根本的解決 URLを出力するときは、「http://」や 「https://」で始まるURLのみを許可する。
根本的解決 <script>...</script> 要素の内容を動的に生成しない。
根本的解決 スタイルシートを任意のサイトから取り込めるようにしない。
保険的対策 入力値の内容チェックを行う。
HTMLテキストの入力を許可する場合の対策 根本的解決 入力されたHTMLテキストから構文解析木を作成し、スクリプトを含まない必要な要素のみを抽出する。
保険的対策 入力されたHTMLテキストから、スクリプトに該当する文字列を排除する。
全てのウェブアプリケーションに共通の対策 根本的解決 HTTPレスポンスヘッダのContent-Typeフィールドに文字コード(charset)の指定を行う。
保険的対策 Cookie情報の漏えい対策として、発行するCookieにHttpOnly属性を加え、TRACEメソッドを無効化する。

上記に書いてない内容としては、IEのContent-Type無視問題くらいです。これも本来ブラウザ側の問題だとは思いますが、IEのシェアを考えると無視できない問題でしょう。以下を参照して下さい。
拙著「体系的に学ぶ 安全なWebアプリケーションの作り方」を読んでいただけば、上記のことは全て具体的に解説しています(PR)。

また、AjaxアプリケーションのXSSについては、基本はHTMLのエスケープとContent-Type無視問題への対処ですが、具体的には以下の記事をお読み下さい。
このAjaxアプリケーションのXSSについては、PHPカンファレンス北海道にて解説する予定です。


[PR]
WAF始めました。詳しくはHASHコンサルティング株式会社まで。
安全なWebアプリケーションの作り方DRMフリーのPDFによる電子版もあります。

2012年4月9日月曜日

PHPのescapeshellcmdを巡る冒険

以前、ブログ記事「PHPのescapeshellcmdの危険性」にて、escapeshellcmd関数の「余計なお世話」によって危険性が生まれていることを指摘しましたが、その後大垣さんによって修正案が提示され、結局「それはマニュアルの間違い」ということで決着が着いたようです。ところが、この議論とは別のところで、escapeshellcmdはPHP5.4.0で挙動が少し変わっていることが分かりました。

経緯

  • 2011/1/1 徳丸が「PHPのescapeshellcmdの危険性」を書いて、クォート文字がペアになっている場合にエスケープしないという仕様が余計なお世話であり、危険性が生じていることを指摘
  • 2011/1/7 大垣さんがブログエントリ「phpのescapeshellcmdの余計なお世話を無くすパッチ」にて修正案を提示
  • 2011/10/23 廣川さんが、大垣さんのパッチ案を少し修正してbugs.php.netに提案。修正案は却下され、マニュアルを修正することに
  • 2012/3/1 PHP5.4.0リリース。実はescapeshellcmdの仕様が変わっていた

MLでの議論

大垣さんと廣川さんのパッチ提案に対して、lbarnaudさんが、「いやいや、escapeshellcmdはコマンド全体をエスケープするもので、提案のような使い方は想定してないよ、マニュアルの例も間違いだ」と指摘します。
歴史的に見て、この指摘は正しいように思いました。そうでないと、escapeshellcmdとescapeshellargという別の関数が存在する理由が説明できません。
すなわち、元々PHPには、コマンド全体をエスケープするescapeshellcmdが存在したが、それだとコマンドの引数を追加する攻撃が出来てしまうので、escapeshellargという関数が後から追加されたのでしょう。
ということで、escapeshellcmdは修正せず、マニュアルの方を修正するということで決着がつきました。詳細は、bugs.php.netの方を参照下さい。マニュアルは既に修正されています
ところが・・・

PHP5.4.0でescapeshellcmdの仕様が変わっていた

このエントリは、上記の顛末を報告するつもりで書き始めたのですが、念のためPHPの様々なバージョンでescapeshellcmdの挙動を確認したところ、PHP5.4.0でこの関数の仕様が変わっていたことが分かりました。

サンプルスクリプトは以下です。
<?php
  echo phpversion(), "\n";
  $a = 'echo "foo    bar" baz"';
  echo $a, "\n";
  echo escapeshellcmd($a), "\n";
  system(escapeshellcmd($a));

PHP5.3.10での実行結果
5.3.10
echo "foo    bar" baz"
echo "foo    bar" baz\"
foo    bar baz"

PHP5.4.0での実行結果
5.4.0
echo "foo    bar" baz"
echo foo    bar baz\"
foo bar baz"

「' および " は、対になっていない場合にのみエスケープされます」ではなく、「対になっている' および " は削除されます」という動作に変わっています。その結果、echoの表示も、複数の空白が一つにまとめられています。マニュアルには、この仕様は記載されていないようですし、変更履歴にも載っていません。
この修正も余計なお世話としか思えませんが、私の結論は変わりません。
ということで、先のエントリの結論を再掲します。

まったく余計なお世話としか言いようがありません。この仕様では、恐ろしくてescapeshellcmdは使えませんし、マニュアルにここまではっきり書いてある仕様を今さら変えられないでしょう。escapeshellcmdはお蔵入りするしかないと思います。幸い、escapeshellargの方はまともな仕様と思われますので、escapeshellargで代替してください。
ただし、そもそもOSコマンドを呼ぶのがよいかとか、もっと良い方法はないのかという疑問が出てきます。もっと良い方法はあります。それは、本が出てからのお楽しみ、ということで。

追記

廣川さんに確認したところ、上記はPHP5.4.0のバグだそうで、PHP5.4.1で元の仕様に戻るだろうということです。PHP5.4.0でescapeshellcmdを使用する際はご注意ください。廣川さん、確認ありがとうございました。

[PR]
Webサイトのセキュリティ強化策についての相談は、HASHコンサルティング株式会社まで。
安全なWebアプリケーションの作り方DRMフリーのPDFによる電子版もあります。

2012年4月5日木曜日

PHPの組み込み関数で例外を発生させる方法

このエントリではPHPの組み込み関数でエラー時に例外を発生させる方法を紹介します。デフォルト状態では、PHPの組み込み関数の大半はエラー時に例外を発生させません。

前のエントリで、PHPのheader関数は戻り値を返さず、エラー時に例外も発生させないことを紹介しました。これは酷い仕様だと思うのですが、どうすればエラーハンドリングできるかを考えてみました。

header関数の場合、エラー(警告)そのものは出ているので、以下の二つの方法が候補として考えられます。
  • error_get_last関数で直近のエラーを取得してエラー処理する
  • set_error_handlerで定義したエラーハンドラ関数でエラー処理する
どちらもモダンな書き方とはほど遠い感じです。
前者は、BASICのon error resume nextを連想させますし、直近のエラーがどの箇所で起こったかは簡単には識別できないので、過去のエラーを捕捉してしまいそうです。行番号はとれますが、行番号で判断するのはよくないでしょう。これもBASICみたいですね。
一方、エラーハンドラでエラー処理するのもよくありません。エラーハンドラにはあらゆるエラーが飛んでくるので、ログを吐いてプログラムを終了させるくらいしか現実にはできないでしょう。

そういう問題意識で「パーフェクトPHP」を読んでおりましたら、組み込み関数から例外を発生させる方法がちゃんと書いてありました(同書P160)。
また、エラーハンドラを設定することにより、PHPの標準のエラーを例外に変換して投げることもできます。エラーを例外に変換するには、次のようにエラーハンドラを設定します。例外の種類には、定義済みのErrorExceptionを利用しています。
set_error_handler(function ($errno, $errstr, $errfile, $errline ) {
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});
PHPのパワープログラマには常識なのかもしれませんが、私は「なるほどねぇ~」と思いました。ブログ記事などでも見かけないようです。ただし、PHPのマニュアルには書いてありました

これを使って、header関数の例外処理のサンプルを書いてみました。
<?php
// エラーを例外に変換
set_error_handler(function ($errno, $errstr, $errfile, $errline ) {
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});
// リダイレクト関数…ただし、エラーになる
function redirect() {
  header("Location: http://www.yahoo.co.jp\nSet-Cookie: aaa=bbb");
}
try {
  redirect();
} catch (ErrorException $e) {
  echo 'Redirect error: ' . htmlspecialchars($e->getMessage(), ENT_COMPAT, 'UTF-8');
  // 必要な後始末
}
結果は、「Redirect error: Header may not contain more than a single header, new line detected.」と表示されます。
ということで、エラーを例外に変換する方法を紹介しました。パーフェクトPHPは本当に良い本ですね。
ただし、これをやると、警告を含む全てのエラーで例外が発生するので、ちまたでよく見るような、エラー処理もろくにしていないヌルい書き方はできなくなり、プログラム全体できちんと例外の設計をする必要があります。本来そうあるべきですけどね。

追記

はてなブックマークコメントで「$errno == E_WARNING のときは例外投げないようにすれば」というコメントを頂戴しましたが、そうもいかないように思います。
そもそもの発端だったheader関数のエラーはE_WARNINGです。しかし、ヘッダを送信しようとしてできなかったという事象は「WARNING」という語感とは裏腹な、かなり深刻なものだと思います。また、E_WARNINGより重いエラーであるE_ERRORは「重大な実行時エラー。これは、メモリ確保に関する問題のように復帰で きないエラーを示します。スクリプトの実行は中断されます。」(マニュアル)とあるように、捕捉する前に終了してしまいます(実験で確認しました)。
ということですので、色々工夫の余地はあるとは思いますが、E_WARNINGレベルのエラーは捕捉するべきだと思います。

※ $e->getMessage()の箇所にHTMLエスケープが抜けていたので追記しました(2015/4/26)

[PR]
パーフェクトPHP+徳丸本セットを抽選で1名に差し上げちゃうキャンペーン」やってます
安全なWebアプリケーションの作り方」電子書籍版販売しています。電子版はこちら

PHP5.4.0でheader関数の脆弱性が修正された

PHPのheader関数にはHTTPヘッダインジェクション脆弱性がありましたが、PHP5.4.0で修正されていることを確認しましたので報告します。

PHPのheader関数はHTTPレスポンスヘッダを送信するための関数です。元々header関数には改行文字のチェックが入っていなかったので、HTTPヘッダインジェクション脆弱性が入りやすかったのですが、PHP4.4.2 および 5.1.2の修正として、「この関数は一度に複数のヘッダを送信できないようになりました。 これは、ヘッダインジェクション攻撃への対策です。」と、改行文字が入っている場合、レスポンスヘッダを送信しないようになりました(header関数のマニュアル参照)。

しかし、このチェックはラインフィード(0x0A)しかチェックしておらず、キャリッジリターン(0x0D)のみを使ったHTTPヘッダインジェクション攻撃が可能な状態でした。この状況は、拙著「体系的に学ぶ 安全なWebアプリケーションの作り方」のP205にコラム「PHPのheader 関数はどこまで改行をチェックするか」として説明しています。

これに対して、PHP5.4.0で、header関数がキャリッジリターンもチェックするように修正されました。
https://bugs.php.net/bug.php?id=60227に改訂履歴があります。廣川類さんが担当してくださったのですね。報告者として私の名前もあります。廣川さんありがとうございました。
手元の環境で確認したところ、PHP5.3.10では変更なし(キャリッジリターンを許容)、PHP5.4.0ではキャリッジリターンを含むヘッダ全体が送信されなくなりました。

私の本では、header関数の呼び出し側で改行文字をチェックすることを推奨していますが、PHP5.4.0以降でその必要はなくなりました。しかし、まだまだPHP5.3を使うケースも多いと思うので、当面の間は、header関数の呼び出し側で改行文字をチェックした方がよいでしょう。

ところで、header関数は、どうやってアプリケーションにエラーを返すのでしょうか。マニュアルを見ると、header関数はvoid型で、返り値は「値を返しません」とあります。例外も発生しません(PHPの組み込み関数は原則的に例外を発生させません)。
これは酷い仕様だと思うのですが、header関数(およびその他の組み込み関数)で例外を発生させる方法はあります。これについては次のエントリで説明します。

[PR]
パーフェクトPHP+徳丸本セットを抽選で1名に差し上げちゃうキャンペーン」やってます
安全なWebアプリケーションの作り方」電子書籍版販売しています。電子版はこちら

2012年4月3日火曜日

悪いサニタイズ、良い(?)サニタイズ、そして例外処理

先日のエントリ「処理開始後の例外処理では「サニタイズ」が有効な場合もある」は、素材の消化不足、私の表現の未熟等から、一部で誤解を招いてしまったようで申し訳ありません。アプローチを変えて、サニタイズについてもう一度考えてみたいと思います。結論から言えば、悪いサニタイズはあっても、「良いサニタイズ」はないと考えます。しかしながら、状況によっては妥協の産物としてサニタイズを使うことは、あり得ると考えます。

本稿で用いる「サニタイズ」の定義

サニタイズという用語は、歴史的に都合の良いように使われてきた歴史があり、あらためてネット検索して見ると、本当に多様な使われ方をしていると感じました。その様子は、高木浩光氏のブログ記事『「サニタイズ」という言葉はもう死んでいる』からも伺えます。

ここでは、議論の都合上、以下をサニタイズの定義として用いることにします。
サニタイズとは、
主にセキュリティ上の目的で、
処理対象の文字列から、
処理に支障のある記号類を、
除去、あるいは別の文字(マイナス記号など)に置き換えることであり、
処理の行われる箇所(入口か出口かなど)は問わないことにする。
この段階で「いや、サニタイズとはそういうものではない」という意見が噴出しそうですが、前のエントリでは、奥一穂氏と私の間の共通認識は上記のものだったと考えますし、元々サニタイズの厳密な定義などないと思われるので、本稿ではこの定義を用います。
まず、「悪いサニタイズ」について説明します。

悪い例1:入力時のサニタイズ

入力時に、「支障の出そうな文字」をまとめて削除等することがありますが、よくありません。恐らく、サニタイズという用語をもっとも狭義の定義がこの使い方で、以下の定義になると考えます。
外部から入力された文字列から、
処理に支障のある記号類を、
削除あるいは別の文字(マイナス記号など)に置き換えること
「処理に支障のある記号類」としては、 < > ' " ; \ % _ & ? { } ` @ | などが候補ですが、これらに限りません。
2005年くらいまでは、上記の手法が脆弱性対処の主流として盛んに使われてきました。日本独特の手法としては、「別の記号に置き換える」代わりに「いわゆる全角文字に置き換える」という手法も用いられ、一種のサニタイズととらえることができます。
処理が簡便であるために人気のあった手法ですが、以下の問題があり、最近はあまり見かけなくなりました。
  • 脆弱性の発生メカニズムを根拠にしていないので、常に安全とは限らない
  • サニタイズ対象の記号をどう選んだらよいか根拠があいまいである
  • サニタイズ対象の記号を要件として使いたい時に困ってしまう
  • そもそも入力データをセキュリティ上の理由で勝手に改変してよいはずがない
ということで、現在では入力時のサニタイズは完全に否定されています。
しかしながら、例外として、大昔の「セキュリティをまったく考慮していないWebアプリケーション」を急いで脆弱性対処しなければならない時など、状況によってはこの方法をとらざるを得ないという場合もあるかもしれません。

悪い例2:入力時点でエスケープする

先の定義とは外れますが、入力データ時点でエスケープする手法もサニタイズと呼ばれることがあります。すなわち、操作内容ではなく、処理の場所に着目して、入力時点でセキュリティに必要な文字列処理を済ませてしまうことをサニタイズと呼ぶ場合もありますが、現在ではこの方法も否定されています。
その理由は、エスケープの方法は、データの使い方(HTML生成、SQL呼び出しなど)によって変わってくるため、入力時にあらかじめ済ませることは不可能だからです。
PHPのマジッククォートは、主にMySQLのSQL呼び出しやOSコマンドインジェクション対策を想定したエスケープを、入力時に自動的に済ませておく機能と捉えることが出来ます。しかし、脆弱性を完全に予防できるわけではなく、副作用が大きかったことから、PHP5.3で非推奨、PHP5.4では機能自体が削除されました。すなわち、入力時のエスケープがうまくいかないことは、歴史が証明しています。

悪い例3:エスケープ可能なのにサニタイズする

次に、入力時ではなくデータを使う時(出力時)に話題を移します。出力時に、記号類をエスケープする手段が提供されている場合(HTMLやSQLなど)はエスケープで対応すべきですが、サニタイズ(記号の削除など)で対処する場合があります。これも好ましくありません。
かつてEvernoteにXSS脆弱性が指摘された際、EvernoteのXSS対策として、一部の記号(「<」、「>」など)が削除されていました。このあたりの経緯は、ブログ記事「Evernote XSS事件のエクスプロイトとその対策過程と顛末」に詳しく報告されています。記事にあるように、「\」のエスケープが漏れていたためにXSS脆弱性が残ってしまいました。
出力時のサニタイズが悪い理由として以下があります。
  • 脆弱性混入の原理を把握しないで対処しているので漏れが生じやすい
  • サニタイズ対象の記号を使わなければならない場合に対応できない

次に、サニタイズの仕様が許容される(かもしれない)ケースを説明します。

許容例1:エスケープの手段がない場合

出力時はエスケープでの対処が基本ですが、エスケープ手段が提供されていない場合があり、その場合はサニタイズが対処の候補になります。
例えば、phpMyAdminのセットアップスクリプトには、利用者が入力した注記をPHPのコメントとして書き出す機能があります。たとえば、「1st server」という入力に対して、「/* 1st server */」と出力するという具合です。しかし、以下が入力されると、コメントが勝手に閉じられてしまい、スクリプトの注入が可能になります。
*/ 任意のPHPコード; /*
出力されるコメントは以下となり、外部から任意のスクリプトが注入できます。
/* */ 任意のPHPコード; /* */
これに対して、PHPのコメント機能には、「*/」をエスケープする手段が提供されていないので、phpMyAdminは「*」を「-」に変換することで、スクリプトの注入を防いでいます。「サニタイズ」後のコメントは以下となり、スクリプトの注入を防止します。
/* -/ 任意のPHPコード; /- */
このあたりの詳細については、私のブログ記事「phpMyAdminにおける任意スクリプト実行可能な脆弱性の検証」を参照ください。

しかし、やむを得ないとは書きましたが、好ましいわけではありません。そもそも、利用者の入力をPHPのコメントとして残すという仕様があぶなっかしい感じですし、それ以前に設定ファイルをPHPのスクリプトとして生成するという仕様も、セキュリティ上の問題が発生しやすいので好ましくないと考えます。

許容例2:表示できない文字を扱う場合

次にありそうなケースとして、処理できない文字が何らかの原因で入ってしまったケースです。ありそうなケースとしては、以下があります。
  • PCとケータイ(ガラケー)の両方に対応しているサイトで、ケータイでは表示できない文字を表示する場合
このような場合、表示できない文字を「〓」(いわゆるゲタ)等の代替文字で置き換えることをします。一般的に、表示できない文字を代替文字に置き換える処理を「サニタイズ」とは呼ばない気がしますが、冒頭の私の定義には該当すると考えます。

許容例3:予防的対策の一環として

今までの例は、エスケープできない種類のデータ形式で、かつその文字を仕様として許容している場合でしたが、このような例は希であって、通常エスケープ手段が提供されていない場合は、その文字は仕様として拒絶しなければなりません。そのような例として以下があります。
  • メールの宛先やタイトル欄の改行文字
  • リダイレクト先URLの改行文字
  • 数値項目中の数値以外の文字
これらは全てプログラムの入口でバリデーションとして入力値をチェックして、適切なエラーメッセージや再入力への誘導をすべきです。しかし、必要なバリデーション処理が抜けてしまったというケースはあり得ます。
このように、受け付けてはいけない(受け付けないはずの)文字が混入してしまった場合、即座に停止するのではなく、対象文字をサニタイズしてでも処理を進めた方が良い場合もある、というのが前回エントリの結論でした。

サニタイズは例外処理後の対処の一方法

これに対して、何人かの方から、サニタイズというのは例外を捕捉した後の対処の方法として捉えるべきだという指摘をいただきました。「受け入れてはいけない文字を受け入れてしまったので処理を継続できない」という状況は、確かに例外処理として扱うべき問題ですし、先のエントリで引用したfacebook上の会話にも、ブログタイトルにも例外処理という言葉は出て来ていたのに、そこを詰め切れていませんでした。
ここで、例外処理を一般化すると、以下のようになると考えます。
  • 例外が発生するかもしれないことを宣言する(try)
  • 処理実行中に例外の発生条件を検知する(数値以外の文字が…など)
  • 例外を発生させる(throw)
  • 例外を捕捉する(catch)
  • 必要な後始末をする(SQLの呼び出しをやめロールバックする…など)
  • try文を終了する
例外処理としてのサニタイズは、本来上記のようにしっかり例外処理を構成しなければならないところの、簡便法としてとらえることができます。
先に許容例2で挙げた「表示できない文字」についても、PerlやRubyのencodeメソッドは、対象外文字を例外として扱えるようになっていて、オプション設定で自動的に代替文字を表示できるようにもなっています。すなわち、例外時のサニタイズは、
  • 本来例外を発生させるべきだが、煩雑な場合もあるので、自動例外処理として代替文字への置き換えができるようになっている
ととらえることができます。

方式設計時に例外処理の方針を固めよう

先のエントリでは、処理が途中まで進んでしまった状況で、「受け入れてはいけない文字を受け入れていることが判明した」場合について、単に処理を打ち切るのではなく、サニタイズしてでも処理を継続した方がよい場合があると書きました。これは、例外処理という観点からは、
  • 受け入れ不可能な文字があるという例外が発生した
  • 例外処理として、受け入れ不可能な文字を削除して処理を継続する
ということになります。しかし、この方法がベストとは限りません。むしろ、「即座に終了よりはマシな簡便法」と言えるでしょう。一番まずいのは、「処理途中で受け入れ不可能な文字があれば、サニタイズして先にすすめばよいのだ」と決めつけてしまうことです。
そうではなく、処理の性質や使う言語の特性などを考慮して、例外発生時の処理方法を方式設計(アーキテクチャ設計)時に検討して、プロジェクトの標準として決めることが重要でしょう。
また、例外捕捉後の処理では、おもに「なかったことにする(ロールバック)」か、「辻褄を合わせて処理を継続する」かの選択になるかと思われますが、具体的な内容は、元々の処理内容に合わせてケースバイケースで決めるべきです。その際には、先に紹介した「例外処理の粒度」、「例外処理伸すコープ」という考え方も有効でしょう。

まとめ

このエントリの前半では、サニタイズという用語を仮に定義した上で、悪いサニタイズと、(良いとは言えないまでも)許容可能なサニタイズを例示しました。
後半では、「許容可能なサニタイズ」が、実は、「処理開始後に受け入れてはいけない文字が現れたという例外」であり、例外処理の処理形態の1つとしてとらえることができることを示しました。しかし、サニタイズが常にベストというわけではもちろんなく、処理をロールバックするか継続するかを含めて、アプリケーション要件と実装方針、開発言語の機能などから、例外処理の方針をきめるべきだというのが結論です。

補足:やはり、「サニタイズ」という言葉はもう死んでいる

このエントリを書くにあたってあらためて「サニタイズ」の用例を調べましたが、予想以上に意味の幅が広い状況でした。サニタイズとはエスケープのことだと説明しているエントリもあれば、入力時に一括して記号類を処理(エスケープも含めて)してしまうことをサニタイズと呼んでいる例もあります。佐名木さんの「セキュアWebプログラミングTips集」では、「バリデート+エスケープ=サニタイズ」という節があるくらいです(同書P126)。
このような状況では、仮に定義して用いたとしても、読者毎に「サニタイズの語感」が元々異なっている以上、誤解を招く危険性は非常に高いと言えます。
したがって、そもそも幅広い読者を想定したエントリでサニタイズという用語を用いてしまったこと自体が不用意でした。混乱を招くような用語を使い、申し訳ありません。私は、今後サニタイズという用語をできるだけ使わないようにしたいと思います。

フォロワー