[PR]小規模ECサイトに最適なWAF、SiteGuard Lite
徳丸浩の日記
2010年07月01日
_ぼくがPDOを採用しなかったわけ(Shift_JISによるSQLインジェクション)
PHPのデータベース・アクセス・ライブラリPDOは、DB接続時の文字エンコーディング指定ができないため、文字エンコーディングの選択によっては、プレースホルダを使っていてもSQLインジェクション脆弱性が発生します。
追記(2011/06/19)
ここに来て急にブクマが追加されはじめていますが、このエントリを書いてから状況が改善しています。PHP5.3.6(2011/03/17)にて、PDOでもデータベース接続の文字エンコーディングを指定できるようになりました。この版で、UNIX版のPHPでは解決しましたが、Windows版のPHPではバグがあり、解決には至っていませんでした。その後、千葉征弘さん(@nihen)により原因が判明し、PHP5.3.7RC1では修正されていることを確認しています。したがって、PHP5.3.7の正式版が出れば、この問題は解決すると思われます。(追記終わり)
はじめに
本を書いています。SQLインジェクションの節から書き始めて、初稿はレビュアーの方々にお送りしましたところ、さっそく有意義なコメントを多数頂戴しています。ありがとうございます。
その原稿の中で、DBアクセスに使用するPHPのライブラリとしてMDB2を紹介していたところ、PDOを紹介すべきではという意見を複数頂戴しました。そこで、現時点でPDOを採用していない理由を報告したいと思います。これは、「安全なSQLの呼び出し方」でPDOを取り上げていない理由でもあります。
PDOは接続時に文字エンコーディングを指定できない(指定しても無視される)ので、データベースへの接続時の文字エンコーディングはLatin1が暗黙に指定されます。すると、日本語の読み書きで文字化けが生じるため、「SET NAMES SJIS」により、文字エンコーディングを接続後に指定するようなプログラミングがなされているようです(MySQLの場合)。
そのようなプログラム例を示します。このプログラムでは、内部文字エンコーディングとしてShift_JISを利用しているという想定です。PHP5.3.0とMySQL5.1.37の組み合わせで確認しました。
<?php $dbh = new PDO('mysql:host=localhost;dbname=test;charset=sjis', 'username', 'password'); $dbh->query("SET NAMES sjis"); $sth = $dbh->prepare("select * from test WHERE name=?"); $sth->setFetchMode(PDO::FETCH_NUM); $name = ... $sth->execute(array($name)); while ($data = $sth->fetch()) { var_dump($data); }
PDO+MySQLの場合、プレースホルダは「動的プレースホルダ」あるいは、俗に「クライアントサイドのプリペアードステートメント」などと呼ばれる実装になっています*1。
このため、$nameとして「ソ' OR 1=1#」という文字列を指定した場合、MySQLに対して以下のようなクエリが送信されます(Wiresharkによるダンプ)。
0x83は「ソ」の先頭バイトです。これをPDOではLatin1(ISO-8859-1)として扱うので、0x83はNBHという制御文字を意味し、1バイト文字になります。PDOは、続く「\'」を「\\\'」とエスケープします。
一方、MySQLサーバー側では、受け取ったSQLをShift_JISと想定しているので「ソ\\'」という並びと解釈します。「\\」は「\」をエスケープしたものなので、後続の「'」は文字列リテラルの終端に使われ、残りの「 OR 1=1#'」はSQL文の一部とみなされます。SQL文が注入できたので、SQLインジェクション脆弱性があるということになります。ここまでの説明を下図にまとめました。
my.cnfの修正でもダメ
「set names」がセキュリティ上問題があることが広く知られるようになったせいか、PDOの文字化けを別の方法で対処するようにすすめているエントリもあります。たとえばこのエントリでは、my.cnf(my.ini)の修正でPDOの文字化けに対応しているのですが、Shift_JISを使う場合はこれもダメです*2。その理由は、「PDOはLatin1として処理したものを、MySQLはShift_JISとして解釈する」状態には変わらないからです。
まとめ
- PDO+MySQLでは、動的プレースホルダ(なんちゃってプリペアード・ステートメント)が使われる
- PDOでは文字エンコーディングが指定できない
- PDO+MySQL+Shift_JISでは、プレースホルダを使ってもSQLインジェクション脆弱性となる
解決策・回避策
PDOの文字化け問題とSQLインジェクション脆弱性問題を解決するにはどうすればよいでしょうか。私が思いつくのは以下の2つの方法です。
- PDOではなくMDB2など文字エンコーディングを正しく扱えるライブラリを使用する(根本的対策)
- アプリケーションからDBまで一貫してUTF-8で処理し、set names utf8により文字化けを回避する(回避策)
2.で具体的な問題が起こるかどうかは分かりませんが、文字エンコーディングを正しく扱えないことで潜在的なリスクがあると考えます。そのため、書いている本の中では1.を採用したという訳です。
しかし、私自身PHPを熟知しているわけではないので、ひょっとすると、PDOの使い方などでもっとよい方法があるかもしれません。識者のご指摘をいただければ幸いです。
追記(2010/07/01 22:20)
id:nihenさんが、PDOに対して文字エンコーディングを設定する方法を調べてくださいました。nihenさん、どうもありがとうございます。以下に引用します。
PDOからよばれるreal_escape_stringで文字コードを考慮させたい場合は $dbh = new PDO('mysql:host=localhost;dbname=sandbox;charset=cp932', 'sandbox', 'sandbox', array( PDO::MYSQL_ATTR_READ_DEFAULT_FILE => '/etc/mysql/my.cnf', PDO::MYSQL_ATTR_READ_DEFAULT_GROUP => 'client', )); もしくはserver-side-prepareを使う場合(文字コード気にしなくておk) $dbh = new PDO('mysql:host=localhost;dbname=sandbox;charset=cp932', 'sandbox', 'sandbox', array( PDO::ATTR_EMULATE_PREPARES => false, )); ちなむと前者は接続時に使われるオプションなのでnew PDOしたあとにあとから $dbh->setAttribute しても、ダメ。後者はおk。
ご覧のように、MySQLサーバーへの接続時に、MySQL APIに渡すパラメータとして、my.cnfのファイル名を指定しています。上記の例ではGROUPを「client」に設定していますので、my.cnfに以下の設定を追加することになります。
[client] default-character-set=sjis
ただし、Windows版のPHPでは、PDO::MYSQL_ATTR_READ_DEFAULT_FILEが使えないようです。このため、サーバー接続時に文字エンコーディングを指定する方法がありません。このため、Windows版のPHPを使う場合は、PDO::ATTR_EMULATE_PREPARESをfalseにする(真のプリペアード・ステートメントを使う)ことで対策することになります。
*1 高木浩光氏のいわれる「なんちゃってプリペアド・ステートメント」
*2 元エントリはUTF-8なので問題はおきませんが、逆に言えば、set names utf8でも問題ないことになります
2010年04月06日
_PROXY(プロキシ)経由でのDNSリバインディングと対策
このエントリでは、PROXY経由でWebアクセスしている場合のDNSリバインディング対策について考察する。
PROXY(プロキシ)経由でWebアクセスしている場合、DNSによる名前解決はブラウザではなくPROXYにより行われる。したがって、 ブラウザ側ではDNS Pinningできないので、PROXY側での対策が重要になる。
広く使われているPROXYサーバーであるsquidの場合、DNS Pinningに相当するパラメータは、negative_dns_ttlであり、デフォルト値は1分間である。したがって、最初のアクセスから1分あけてXMLHttpRequestすれば、DNSリバインディングが成立する。これは実験で確認した。Internet ExplorerやOperaは、他のブラウザと比べてDNS Pinningがしっかりされている印象があるが、PROXY経由の場合は、これらブラウザでも比較的簡単にDNSリバインディングが成立するので注意が必要だ。
先のエントリで説明したように、JavaアプレットによるDNSリバインディングでも、URLクラスを使いかつPROXY経由でのアクセスの場合は、やはりPROXYでのDNS Pinningが問題となる。PROXY経由でのアクセスでは、Javaアプレットの場合でも、JavaScriptのXMLHttpRequestの場合同様、1分以上間隔をあけてやればDNS Rebindingが成立する。ただし、攻撃に使用できるプロトコルはHTTPのみであるので、JavaアプレットとJavaScriptでは、リスクは変わらない。
negative_dns_ttlの値を大きくすれば、squidがDNSキャッシュする最低時間を延ばすことが可能だが、二つの点で問題がある。
第一の問題は、negative_dns_ttlを大きくすることの副作用だ。negative_dns_ttlは元々、DNSアクセスに失敗した場合のキャッシュ保持時間を意味するからだ。以下に、negative_dns_ttlのリファレンスを引用する。
Time-to-Live (TTL) for negative caching of failed DNS lookups. This also sets the lower cache limit on positive lookups. Minimum value is 1 second, and it is not recommendable to go much below 10 seconds.
[squid : negative_dns_ttl configuration directiveより引用]
このため、一例として、negative_dns_ttlを6時間に設定した状態でDNS参照に失敗した場合(参照先DNSサーバーが一時的に停止した場合など)も6時間はDNSを再アクセスしない。すなわち、DNSのエラーが発生した場合は、negative_dns_ttlで設定した時間内は、当該サイトにアクセスできなくなるのだ。このため、negative_dns_ttlの値をあまり大きくすると、サイト閲覧に支障がでる可能性がある。
第二の問題は、negative_dns_ttlを長くしてもDNSリバインディングの根本対策にはならないことだ。 その背景には、PROXYサーバーをマルチユーザで使う場合が多いことや、タブブラウザの普及などがある。 やはりnegative_dns_ttlを6時間に設定した場合で説明しよう。AさんとBさんが同じPROXYサーバーを共有しているとする。Aさんが10:00にワナサイトにアクセスした場合、PROXYは16:00までワナサイトのIPアドレスをキャッシュする。したがって、Bさんが15:59にワナサイトを閲覧すると、そこから一分間でDNSキャッシュが無効になり、Bさんのブラウザ上動作するXMLHttpRequestがプライベートアドレスにアクセスすることを許してしまうかもしれない。 また、シングルユーザで考えても、タブブラウザのユーザはタブを開きっぱなしにする傾向があるので、6時間後の攻撃が成立する可能性はある。
アクセス制御による対策
結局negative_dns_ttlによる対策はうまくいかないので、別の方法を考える必要がある。有効な対策は、proxyのアクセス制御(ACL)により、squidにプライベートアドレスを中継させないことだ。以下に、192.168.0.0/16に対してアクセスを禁止する場合のsquid.confの設定例を示す。
acl localnet dst 192.168.0.0/16 http_access deny localnet
この場合、プライベートアドレスに対してはPROXY経由でアクセスできなくなるので、ブラウザのPROXY設定の例外設定をお忘れなく。下図に例外設定の例を示す。
このACLによる対策の効果であるが、JavaScriptとJavaによるアクセスに関しては完全に対策できると考える。当初Javaアプレットが対策から漏れることを心配していたが、先のエントリで検証したように問題ないことがわかった。Flashについては未検証なので今後検証したい…ところだが、私はFlashのコンテンツを書いたことがない。どなたか教えてください。
まとめ
PROXY経由でWebアクセスしている場合のDNSリバインディング対策について検討し、PROXYのACLによりプライベートアドレスを中継禁止することで対策可能であることを示した。DNSリバインディング対策のまとめについては別稿にて報告する。
2010年04月05日
_JavaアプレットのDNSリバインディングはJRE側で対策済みだった
Black Hat Japan 2007における金床氏のプレゼンテーションや金床本で、Javaアプレットに対するDNSリバインディングの手法が説明されている。ここしばらく、その追試を行っていた。結論としては、金床氏の方法にはJRE側で対策がとられていたので報告する。
金床氏が報告しているDNSリバインディングの手法とは以下のようなものだ(金床本に説明されている方法)。
- 被害者のユーザにワナのページをPROXY経由で閲覧させる
- ワナのドメイン(FQDN)に対するIPアドレスを変更する
- ワナページ上で数秒~数十秒のちにJavaScriptによりJavaアプレット用のHTMLをレンダリングする
- JREがアプレットをPROXY経由で*1ダウンロードする
- アプレットのダウンロード時点で既にDNSレコードは書き換わっているが、PROXYがDNSキャッシュしているため、アプレットは正常にダウンロードされる
- アプレットからワナサイトのFQDNの任意のポートに対してSocket通信する。
- 既にIPアドレスは書き換わっているので、プライベートアドレスに対して任意のSocket通信ができることになる。
このように、攻撃が成立すればとても凶悪な攻撃となるのだが、最近のJREでは以下の方法で対策されているようだ。
- アプレットがPROXY経由でダウンロードされた場合、アプレットからのSocket通信は全てクロスドメイン通信と扱われる
このような挙動に変わったバージョンを調べたところ、Java SE 6 update 3以降であることが分かった*2。すなわち、Javaアプレットの金床方式によるDNSリバインディングが成立するバージョンは以下の通りである。
- J2SE1.4.xの全てのアップデート
- J2SE5.0の全てのアップデート
- Java SE 6のUpdate2以前
そして、Java SE6 Update3以降で、クロスドメイン通信になった場合の挙動は、JREのバージョンにより異なる。Java SE 6 update 10より前のバージョン(Update3~7)だと、Socketでダイレクトにアクセスしようとするとエラーになる。一方、Java SE 6 update 10以降のバージョンでは、アクセス先のcrossdomain.xmlを参照して、その内容に従う。このあたりの挙動は、「次世代 Java(TM) Plug-In テクノロジのリリースノート (JDK 6u10)」を参照されたい。
一方、アプレットが直に(PROXYを経由せずに)ダウンロードされた場合は、アプレットの置かれたFQDNに対してSocketによる任意ポートへの通信が可能だ。この場合は、DNSリバインディングが懸念されるところであるが、Javaの強固なDNS Pinningにより、アプレットダウンロード時のIPアドレスが永続的に保持されるので、DNSリバインディング攻撃はできない。
したがって、金床氏のプレゼンテーション資料には、Java版DNSリバインディング対策が色々書かれているが、その後Java側で対策がとられたことを考慮すると、ユーザが取るべき最善の対策は以下だろう*3。
- JREを最新のバージョンにアップデートする
J2SE5.0以前のバージョンは既にEOLになっているし、DNSリバインディング対策もとられていないことから、使うべきではない。
また、Socketクラスではなく、URLクラス等HTTP系のクラスを使う場合は、PROXY経由の有無にかかわらず元FQDNとの通信は可能である。URLクラスを使う場合は、アプレットダウンロード時の設定に従い、PROXY経由となるか否かが決まる。URLクラスで、PROXY経由しない場合は、JavaのDNS Pinningが有効となり、DNSリバインディングはできない。一方、PROXY経由の場合は、PROXY側のDNS Pinningが問題となる。PROXYによるDNSリバインディング対策については稿を改めて説明する。
*1 Javaの設定にもよるがデフォルトはブラウザの設定をそのまま利用するので、PROXY経由となる
*2 Java SE 6 update 3がリリースされたのが2007年10月3日、金床氏のBHJでのプレゼンテーションが2007年10月25日だったので、BHJのプレゼンテーション時点でJRE側の対策がなされていたことになる。ただし、update 3のリリースノートを読んでも、このような対策がなされていることは読み取れなかった。おそらく金床氏もupdate 3で対策されたとは明確には分からなかったのだろう。金床氏のJavaアプレットによるDNSリバインディングのデモページには「Does not work on JRE1.6.0_03 or later」と明記されている。
*3 今時Javaアプレットなど使わないというのであれば、あわせてJavaアプレット(プラグイン)を無効にするというのもあり。その場合もJREのバージョンは最新にすべきだ。
2010年03月29日
_DNSリバインディングによる無線LANパスフレーズの読み出しに成功
DNSリバインディング攻撃により、無線LANアクセスポイント(AP)に設定したWPAのパスフレーズ(PSK)を読み出す実験に成功したので報告する。
先のエントリではDNSリバインディング攻撃によるルータ設定変更の実験を報告したが、エントリでも触れたように設定変更についてはCSRF脆弱性を悪用することによっても可能だ*1。両手法の違いとして、CSRFは攻撃先の情報を読み出すことはできないのに対して、DNSリバインディングでは読み出しも可能だ。DNSリバインディングによるセッションCookieの読み出し例については、「ケータイtwitter(twtr.jp)においてDNS Rebinding攻撃に対する脆弱性を発見・通報し、即座に修正された」でも言及した。
無線LANアクセスポイントに対するDNSリバインディングのリスクを考える上で、もちろん設定変更もリスクではあるが、設定変更された場合、元々の所有者が無線LANにアクセスできなくなるなどの副作用が想定される。一方、パスフレーズの読み出しが行えれば、被害者のユーザに気づかれないままに攻撃される可能性が高く、それだけリスクも大きくなる。
また、Wi-Fiの普及に伴い、ゲーム機や携帯電話などでも広く無線LANが利用できるようになったことを受けて、無線LAN機器メーカー各社は無線LANの簡易設定方式を提唱・実現している。これはよいことだが、一方で、誰も無線LANの設定画面を見たこともなく、APの管理用パスワード(WEPやWPAのパスフレーズではない)がデフォルトのままになっているケースが大半であると予想される。これは危険な状態ではないか。
AOSSによる簡単設定
筆者が実験用に所有しているAPは、株式会社バッファローのWLA2-G54Cである。auの携帯電話biblioのWi-Fi機能を評価するために購入したものだ。バッファロー製APの特徴としてAOSSという簡単設定機能により、ボタン一つで無線LANの設定が可能である。その一端を以下に示そう。
APの設定でまず行うことは、APのIPアドレスの設定だ。ブラウザでもできなくはないが、APのユーティリティを使用すれば簡単だ。
上図でパスワードを入力する欄があるが、これは現在の管理パスワードを入力する欄でデフォルトは空である。ここで管理パスワードを設定できるわけではない。その後、バッファローの接続ユーティリティからAOSSによるプロファイルの追加を指定すると以下のような画面となる。
この状態でAPのAOSSボタンを長押しすると、APとクライアントのネゴシエーションが始まり、SSIDや暗号化の方法、パスフレーズなどが自動的に設定される。
安全な設定が簡単に行えるのだが、この手順に従うと、APの管理画面を一度も起動することなく、またAPの管理パスワードを設定する機会もないのだ。AOSSはPC以外に、ゲーム機(ニンテンドーDSなど)や携帯電話でも利用できるもので、おそらくバッファロー製無線LAN製品のユーザの大半が、管理パスワードを設定しないままに使用していると予想される。
APの管理画面からパスフレーズが見える
この状態で、APの管理画面をブラウザから表示させる。無線LANのメニューを表示すると以下のような画面だ。
これを見ていやな予感のしたあなたの勘は正しい。HTMLソースを表示させると以下のような部分がある(一部マスク表示にした)。
<input name="wl_wpa_psk" type="password" maxlength="64" size="56" value="7c188cXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX67">
すなわち、管理画面が起動できれば、パスフレーズを見ることができるのだ。実は、これはこれで便利な面もあって、AOSSで設定したパスフレーズを確認することにより、AOSS対応でないクライアントを手動設定できるのだが、そういう利用方法を意図したものとも思えない。ともかく、ここまで紹介した内容から以下のことが分かる。
- AOSSによる設定では管理画面を見る必要がない
- 管理画面のデフォルトパスワードは空である
- 管理画面にログインすると無線LAN暗号化のパスフレーズが参照できる
これらの事実から、バッファロー製AP WLA2-G54Cは、DNSリバインディング攻撃により無線LAN暗号化パスフレーズが読み出せると判断し、実験してみることにした。BCNの調査によると、バッファロー社は無線LAN分野の2009年のシェアが53.0%ということで、影響も大きいと思った。
侵入実験の実際
以下に、侵入実験に用いたスクリプトの一部を示す。ドメインは仮のものに置き換えた
// リクエスタのセット var requester = new XMLHttpRequest(); requester.open('GET', 'http://root:@wlan.example.com/advance/ad-lan-wireless_sec_g.htm'); ... // パスフレーズの切り出し var response = requester.responseText; var regexp = /wl_wpa_psk.*value="([^"]+)"/m; if (response.search(regexp) != -1) { pass = RegExp.$1; }このスクリプトはインターネット上のワナサイトに置かれる。このコンテンツが読み出された直後に上記FQDNに対するIPアドレスをAPのプライベートアドレスに書き換える。コンテンツ読み出しから4分後に上記リクエストが送出されるようにスクリプトを作成した。実行例を以下に示す。
図示したように、パスフレーズが読み出されていることが分かる。この後、ワナサイトにこの値をPOSTして、攻撃は完成である。
対策
バッファロー製AP WLA2-G54Cをマニュアルの指示通りに設定した状態において、DNSリバインディングにより暗号化パスフレーズ(PSK)を読み出すことに成功した。ユーザ側の対策としては、以下に尽きるだろう。
管理パスワードを適切に設定する
しかし、コンシューマ向け製品ということを考慮すると、現在の製品仕様のままでは全てのユーザに管理パスワードを徹底させることも難しいだろう。したがって、製品提供者側の対策が今後は重要となる。以下のような対策案が考えられる。
- APの管理パスワードの初期値を機器毎に違うものにする(根本的対策)
- 製品設定の流れの中で管理パスワードの変更を強制する(根本的対策)
- 管理画面URLのホスト部は数値IPアドレスのみを許容する(保険的対策)
まとめ
バッファロー製AP WLA2-G54Cを対象として、DNSリバインディング攻撃により暗号化パスフレーズ(PSK)を外部から読み出せることを確認した。他社製品の場合は、パスフレーズが読み出せるかどうかは不明であるが、マニュアルを読む限りは管理パスワードの初期値が空で、変更も要求されないものが多いよう*2なので、仮に暗号化パスフレーズが読み出せないとしても、設定変更は可能である可能性が高い。この場合、APの設定を「暗号化なし」に設定変更することにより、内部ネットワークへの侵入や盗聴などが行える。
無線LANの簡単設定機能は、AOSS以外にも「らくらく無線スタート」やWPSなどがある。これらは、安全な無線LAN設定を簡略化するという意味で社会的な意義のあるものだと思うが、基本的な想定として、「管理画面にはインターネットからアクセスできないので管理パスワードは設定しなくても大丈夫」と考えられているように見受ける。しかし、DNSリバインディングやクロスサイト・リクエスト・フォージェリ(CSRF)などの受動的攻撃により、インターネット越しに管理画面にアクセスすることは可能だ。ルータにCSRF脆弱性が多数指摘されてきた歴史は、「site:jvn.jp クロスサイト・リクエスト・フォージェリ ルータ」でGoogle検索してみると直ぐ分かる。DNSリバインディングについては、パスワードの設定さえすれば対策できるため、製品の脆弱性としては指摘しにくいが、今後十分発生し得る脅威として、メーカー側には対策を要望する。
2010年03月25日
_DNSリバインディングによるルータへの侵入実験
このエントリでは、DNSリバインディング攻撃によりブロードバンドルーターの設定を外部からの侵入を許すように変更してみたので報告する。
DNSリバインディングに対する関心が高まってきている。本年1月12日には、読売新聞夕刊一面トップで、iモードブラウザ2.0のDNSリバインディングによる不正アクセスが報じられた。3月17日のcomputerworld.jpでは、『2010年に最も警戒すべきセキュリティ脅威は「DNSリバインディング」』という翻訳記事が紹介された。実は「最も警戒すべきセキュリティ脅威」というタイトルは誤報だったわけだが*1、DNSリバインディングというマニアックな攻撃手法が注目を浴びるきっかけにはなったと思う。
そこで、専門家の方にはいまさらかもしれないが、DNSリバインディング攻撃により、自宅のブロードバンドルーターの設定を変更してみた。思いの外簡単にできたが、対策も容易であるので、その両方を説明しよう。
侵入実験の前提
まず一般的なDNSリバインディングの概要説明は、「DNS Rebinding」を参照頂きたい。この説明では、イントラネット内のサーバーへの侵入の例を挙げているが、今回の実験では、ブロードバンドルータへの侵入となる。実験に用いた自宅のルータは、プライベートアドレス(内部ネットワーク)からは設定が変更できるが、インターネット側からは設定できないという仕様となっている。そのため、DNSリバインディング攻撃が有効だ。この種のルータに対する攻撃手法としてはクロスサイト・リクエスト・フォージェリ(CSRF)という選択肢も考えられるが、このエントリでは言及しない。
実験では、まず、ルータの設定変更に必要なHTTPリクエストを調査した。その結果、POSTリクエストを3回送信すれば、外部からの侵入を許すように変更できることがわかった。
次に、インターネット上にワナサイトを用意して、ワナサイトをリクエストした後にDNSの内容を書き換え、一定時間置いた後に上記3つのPOSTリクエストを送出するようにした。実験に用いたブラウザはFirefox 3.6.2であり、最初のリクエストから4分間の間隔をとれば、DNSリバインディングが成立することがわかった*2。このため、3回のリクエストはそれぞれ、4分後、4分10秒後、4分20後に送出されるように、JavaScriptによるスクリプトを作成した。
ここで問題になるのが、ルータの認証をどうやってすり抜けるかだ。他の攻撃手法、たとえばCSRFの場合は、ユーザが認証している状態を悪用するわけだが、DNSリバインディングではこの方法は使えない。なぜなら、DNSリバインディングによるリクエストのHost:フィールドはワナサイトのドメインになっているからだ。このため、実験では、攻撃者がルータのパスワードを推測できるという想定にした。以下の説明では、ユーザIDとパスワードがともに「admin」になっていると想定する。これはそれほど突飛な想定ではない。この種のルータの初期設定はパスワードなしか、固定の初期パスワードになっていることが多いからだ。
侵入実験の実際
下図に侵入前のルータの設定を示す。赤い四角で囲った部分に、新たな設定を追加する。
次に、以下は侵入実験に用いたスクリプトのごく一部だ。このルータはBASIC認証が使われているので、以下のようにURLの中にBASIC認証のIDとパスワードを記述した。スクリプトキディが真似するとまずいので、スクリプトの全容は非公開とする。また、router.example.comはワナサイトのドメインだが、仮にものに置き換えた。
var requester = new XMLHttpRequest(); requester.open('POST', 'http://admin:admin@router.example.com/entry_ipnat_main_bottom.html');
このスクリプトを含むワナが閲覧されたら直ぐに、router.example.comのIPアドレスを192.168.0.1(ルータのアドレス)に書き換える。その後3回のリクエストが正常にルータに送信されると、設定を反映するためにルータがリセットされた。攻撃成功である。攻撃成功後のルータの設定を下図に示す。
ご覧のように、192.168.0.10の端末に対して、インターネットからTCP3389ポートを受け付けるようになった。3389はWindowsのリモートデスクトップが使用するポートなので、この設定変更により外部からの侵入が可能になった。
対策
ルータやファイアウォールの設定変更は、DNSリバインディングのデモとしては定番と言えるものだが、実際の結果を見るとギョッとした人も多いのではないか。しかし、幸いなことに、対策は容易である。前述のように、DNSリバインディング攻撃は、ユーザの認証状態を引き継ぐことができないので、認証がかかっているサイトの場合、パスワードが分からないと侵入できない。従って、良質のパスワードを設定するだけで、侵入を食い止めることができる。
しかし、問題は、これだけブロードバンドのインターネットが普及した状況下で、ブロードバンドルータの多くがデフォルトパスワードのまま(パスワードなしも含めて)の設定と予想されることだ。従って、DNSリバインディングによる攻撃はいつ起こっても不思議ではないし、既に起こっているかもしれない。
従来、ブロードバンドルータのパスワード初期設定がいい加減だった理由は、「外部からは設定変更できないから安全だ」という意識が働いていたからだと推測する。しかし、ルータに対する外部からの侵入手法は、DNSリバインディングの他、ルータのCSRF脆弱性を悪用するなど複数存在する。一方で、ブラウザ側でDNSリバインディングを完璧に対策するのは困難だ。その理由は、IPアドレスの変更は、DNSの正式な仕様として認められた挙動であるからだ。このため、ルータやファイアウォールの初期パスワードやパスワードの管理について、さらなる工夫が求められる。
さらに網羅的なDNSリバインディング対策については、稿を改めて説明する。
追記(2010/03/31)
本エントリと関連して、「DNSリバインディングによる無線LANパスフレーズの読み出しに成功」を書きましたのであわせてお読みください。2010年02月22日
_ケータイtwitter(twtr.jp)においてDNS Rebinding攻撃に対する脆弱性を発見・通報し、即座に修正された
twitterのケータイ版twtr.jpにおいて、DNS Rebindingによるなりすましを許す脆弱性が発見され、1/15に通報したところ、その日のうちに修正された。以下、その経緯について報告する。
経緯
今年の1月12日に読売新聞の記事が出たのを受けて、現実のサイトはどうなのだろうかと改めて気になった。
NTTドコモの携帯電話のうち、インターネット閲覧ソフト「iモードブラウザ2・0」を搭載した最新29機種を通じて、利用者の個人情報を不正取得される恐れのあることが、専門家の指摘で明らかになった。
同社は携帯サイトの運営者にパスワード認証などの安全対策を呼びかけている。携帯電話の機能が高機能化するにつれ、こうした危険は増しており、利用者も注意が必要になってきた。
[ドコモ携帯、情報流出の恐れ…最新29機種より引用]
私自身も携帯電話でいくつかのサイトを巡回しており、その一つにtwitter.comの日本の携帯電話向けフロントエンドであるtwtr.jpも含まれる。ご存じのように、twitterは政府要人や有名人も数多く使っているし、twitter.comの画面で確認する限り、鳩山由紀夫首相や原口一博総務相などの閣僚の方々は携帯から書き込みをされることも多いようなので、仮に脆弱性があった場合、影響も大きくなると思った。原口総務相に関しては、記者団からの「Twitterの質問を受けて、つぶやきを確認する原口氏」という報道写真も公開されているので、(スタッフ任せではなく)自ら携帯電話を使いこなしてtwtr.jpにアクセスしておられることは間違いないだろう。
手始めにtwtr.jpのIPアドレスを別のドメインにセットして、携帯電話からアクセスすると、正常に画面が表示される(右の写真)。これはまずい。このため、DNS Rebindingを使ってなりすましができてしまわないか調べることにした。当然ながら、不正アクセス禁止法に抵触しないよう、自分のアカウントを使用して確認を行った。
調査の概要
調査にあたっては、自宅の調査用サーバと調査専用のドメインを用いた。DNS Rebindingを使ったかんたんログインのなりすましは、iモードIDを用いた「かんたんログイン」のDNS Rebinding脆弱性を発表する際に実験で確認していたが、twtr.jpのログイン画面は、次の点で私の実験とは異なっていた。
- 実験ではGETメソッドだったが、twtr.jpはPOSTを使用
- twtr.jpはログイン時にトークンの受け渡しをしていた
認証機能は副作用を伴うので本来はPOSTメソッドを使うのが正しいし、トークンは、外部から認証リクエストを強要される行為(CSRFに似ているが、認証前なのでCSRFではない)を防ぐためだろう。デジタルガレージ社の実装はセキュリティ上の考慮がなされていると感じた。
しかし、DNS Rebindingを使用すれば、ログイン画面のトークンを読み出すことが可能である。すぐに確認作業が終わるだろうと思っていたが、意外なところで失敗した。ログイン・リクエストのPOSTがうまくいかないのだ*1。早く確認を終わらせないと、もたもたしているうちに脆弱性を悪用されるとまずい。結局、XMLHttpRequestをあきらめ、IFRAME要素を用いることにした。
最初、IRAME要素をDOMで動的に作ったりしていたのだが、IFRAME内のFORMをうまくSUBMITできない。このため、以下のような構成にした。検証コードを公開すると、スクリプトキディが他のサイトで悪用するといけないので、コードは非公開とする。
一般ユーザ(被害者)からリクエストあり ①DNS情報書き換え → これ以降、ワナサイトは twtr.jpのIPアドレスを指す ・以下はIFRAME内の処理 ②ログイン画面を要求(トークンが含まれる) ③トークン取り出し → ログイン画面のINPUTにセット(写真2) ④ログイン実行 → セッションIDがCookieにセットされる(写真3) ・以下はIFRAME外の処理 ⑤CookieをINPUTにセット ⑥Cookie値を情報収集サーバにPOST 以下は、攻撃者の立場 ・セッションID受信を確認 ・別の携帯電話にCookieをセット ・twtr.jpにアクセス → なりすましを確認 ・書き込みをしてみる → 成功(写真4、写真5)
写真2 |
写真3 |
写真4 |
写真5 |
写真2はトークンを受信した様子、写真3はIFRAME上でログイン後にCookie上のセッションIDをINPUTにセットしている様子である。このセッションIDを別の端末にセットして、書き込みをしてみたところが写真4である。
通報・届出およびデジタルガレージ社の対応
脆弱性を確認したので、twtr.jpの運営元である株式会社デジタルガレージに通報するとともに、IPAの脆弱性届出窓口に届け出た。その経緯を時系列で示す。
2010/01/14 深夜 脆弱性の確認完了 2010/01/15 11:37 デジタルガレージ社への通報 2010/01/15 12:52 IPAへの届出 取扱い番号 IPA#04364080 として受信される 2010/01/15 19:18 IPAより届出受理および取り扱い開始の連絡 2010/01/15 21:51 デジタルガレージ社の担当者より修正済みの返信。手元でも修正を確認。 2010/01/19 10:53 IPAより修正完了の連絡。その後取り扱い終了となる。
下図に、修正を確認した様子を写真で示す。
通知からわずか10時間あまりでの修正である。 高木浩光@自宅の日記 - はてなのかんたんログインがオッピロゲだった件によると、はてなは通知から修正完了まで20日も掛かったようだが(修正内容は異なるものの)株式会社デジタルガレージの対応の素早さは際だっている。
脆弱性の影響範囲
当該脆弱性の影響を受けるユーザは以下の条件を全て満たす利用者である。
- twtr.jpを一度でも使ったことがある
- iモードブラウザ2.0の対応機種(2009年夏モデル以降)の利用者
- iモードIDを通知設定をONにしている(デフォルトはON)
- JavaScriptの設定を有効にしている(デフォルトは有効)
携帯電話のユーザは、大半の方がデフォルト設定で端末を使っていると思われるので、簡単に言えば、iモードブラウザ2.0端末でtwtr.jpを使ったことのあるユーザ、ということになる。
当該脆弱性の影響は、セッションハイジャックによって受ける影響と等しい。すなわち、ワナサイトを閲覧してしまったユーザの当該ユーザでの投稿、ダイレクトメッセージの送信、ダイレクトメッセージの履歴閲覧、自己紹介やプロフィール画像の変更などである。また、「メールでツイートの設定」画面から投稿用メールアドレスを確認しておけば、後からいつでも当該アカウントでのつぶやきが行える。個人情報に関しては、メールアドレスも含めて閲覧できる内容はあまりないようだ。
先にtwtr.jpのユーザの例として挙げた鳩山首相や原口総務相が仮にワナサイトを閲覧させられた場合は、首相や総務相のアカウントで、攻撃者は任意のつぶやきが行えたことになる。
保険的対応
twtr.jpの脆弱性を悪用された書き込みなどは、私の知る限りは公表されていないようだが、脆弱性が修正された今でも油断はできない。仮に、攻撃者がなりすましを成功させていた場合、後から書き込みをする手段があるからだ。それは前述の「メールでツイート」機能を利用する方法だ。これは、その名の通りメール経由でtwitterの投稿をするもので、ユーザ毎の専用メールアドレスに文章を送信すると、その内容がtwitterに投稿される。右の写真は、私の「専用の投稿先メールアドレス」を表示させたものである。このメールアドレスはいつでも変更できるので、twtr.jpユーザは念のため変更しておいた方が良いだろう。
また、twtr.jpはかんたんログインを禁止設定することができない。メニュー上は「各種設定」から「かんたんログインの無効化」というメニューがあるが、試してみたところ、かんたんログインの設定内容を削除するだけで、その後パスワード認証でログインすると、再びかんたんログインが有効となる。このため、ユーザがtwtr.jp上でかんたんログイン設定を残したくない場合は、twtr.jpにアクセスするたびに「かんたんログインの無効化」を実行する(現実的ではない)か、iモードIDを無効化するしかないだろう。iモードIDは、iモードのマイメニューから無効化できる。
利用者がDNS Rebinding攻撃を避ける目的では、JavaScriptの無効化が現実的かつ有効な方法だと考える。
かんたんログイン手法の脆弱性に対する責任は誰にあるのか(再)
デジタルガレージ社がかんたんログインのDNS Rebinding問題を対策していなかった理由は何だろうか。おそらく、単純にこの問題を知らなかったのだろう。無理もない。私が昨年11月に公表していたとはいえ、小さなセキュリティ会社が公表した内容まで含めて、セキュリティ情報をすべて拾い上げて検証・対応しなければならないとするのも酷な話だと思う。しかも、脆弱性を通知したらその日のうちに対策する能力がデジタルガレージ社にあったのだ。IPAの届出制度は、個別のアプリケーションやWebサイトの脆弱性を扱うもので、かんたんログインのような「認証手法」は取り扱い対象外だ*2。このあたり、問題を周知できない自分自身の力のなさを痛感するとともに、やはり携帯電話事業者に『どうやったら「かんたんログイン」なるものが実現できるのか、ちゃんとした実装方法の公式解説を出』してもらわないと、この種の問題はなくならないだろうと改めて感じた。
_ らんさん [ツイッターを初めて登録しようと思いますので、よろしくお願いいたします。]
2010年02月12日 DNSリバインディング
_かんたんログイン手法の脆弱性に対する責任は誰にあるのか
id:ikepyonの日記経由で、NTTドコモのサイトに以下のセキュリティ・ガイドラインが掲示されていることを知った。
iモードブラウザ機能の多様化により、機種によってiモードサイトにおいてもJavaScriptを組み込んだ多様な表現、CookieやReferer情報を有効に活用したサイト構築が行えるようになりました。
しかし、PC向けインターネットサイト同様に、セキュリティ対策が十分に行われていないサイトでは、そのサーバの脆弱性を突き(クロスサイトスクリプティング、SQLインジェクション、DNSリバインディングなど様々な攻撃手法が存在しています)、これらの機能が悪用される危険性があります。十分にご注意ください。
[作ろうiモード:iモードブラウザ | サービス・機能 | NTTドコモより引用]
XSSやSQLインジェクションと並んで、DNSリバインディングというマニアックな手法が紹介されていることは驚きだが、おそらく私が昨年発表したiモードIDを用いた「かんたんログイン」のDNS Rebinding脆弱性のことを指しているのであろう。
しかし、引用部の表現には違和感がある。DNSリバインディング自体は既知の攻撃手法であるが、「PC向けインターネットサイト」に対する攻撃ではなく、インターネットサイトを閲覧しているPC自身やファイアウォールの内側のローカルネットワーク上の端末に対する攻撃手法として知られている。このあたりの解説については、ここやここを参照頂きたい。
しかも、先のセキュリティ・ガイドラインには、これら脆弱性についての説明はなく、「十分にご注意ください」とあるだけで、参考情報としてはIPAのトップページが紹介されている。
なお、セキュリティ対策情報については、「独立行政法人 情報処理推進機構」(IPA)が公開する情報なども参考にしてください。
[作ろうiモード:iモードブラウザ | サービス・機能 | NTTドコモより引用]
IPAにDNSリバインディングの解説があったかなと疑問に思い、「DNS "リバインディング" site:ipa.go.jp」や「DNS rebinding site:ipa.go.jp」などのキーワードで検索してみたが、ヒットしない。ひょっとしたらどこかに存在する可能性はあるが、リンクもなく、サーチでも引っかからないのでは、ないのと同じだ。これでは、単に危険があるから注意しろと言っているだけで、解決策を示していないことになる。
このような状況から、この文書は次のような意図をもって書かれたのではないかという感想を持った。
- 同DNSリバインディングの問題は、携帯端末や事業者設備の問題ではなく、Webアプリケーション側の問題である(
私も同意追記参照) - NTTドコモは、かんたんログインのDNSリバインディング脆弱性を、PCも含め昔から存在していた問題と印象づけたいのではないか(実際は違う)
- DNSリバインディングの解決方法を明確にリンクしなかったのは、「NTTドコモ公認の解決策」という印象を与えることを避けたかったからではないか
- 一方で、DNSリバインディングに対する注意喚起を行ったという実績は作りたいのではないか
「実績作り」で思い浮かぶのは、Yomiuri Onlineにも掲載された以下の記事だ。この記事は、かんたんログインのDNSリバインディング脆弱性に関して、NTTドコモのコメントを掲載している。
NTTドコモでは、公式サイトを運営する約3000社には注意喚起したが、それ以外の無数にある「勝手サイト」には「ジャバスクリプトの安全な利用はサイトを作る側にとって基本的知識であり、具体的に説明はしていない」という。
[ドコモ携帯、情報流出の恐れ…最新29機種より引用]
勝手サイトに対して従来説明していなかったので、新たに説明したということなのかもしれない。しかし、先の説明ではまったく不十分だ
NTTドコモ(および他の携帯電話事業者)は、かんたんログインのセキュリティ問題について、そろそろ態度を明確にする必要があるのではないだろうか。その態度とは、以下の選択肢のどれを選ぶのかということだ。
- かんたんログインという手法は各サイトが独自に実装しているもので、携帯電話事業者はガイドラインなども提示していないので責任は一切負わない
- かんたんログインは、携帯電話事業者が提供している端末固有IDの応用であるので、安全な使い方を示すなど携帯電話事業者としても一定の責任が生じる
くだんの「セキュリティ・ガイドライン」を読む限り、NTTドコモは(本音では)事業者として一切責任を負わないと姿勢なのだと想像する。しかし、そう明言すると反発も予想されることから、あいまいな態度をとっているように見うけられる。
しかし、このようなあいまいな状況がもっとも危険なのだ。かんたんログインがこれだけ普及した現在でも、この手法に対する責任がどこにあるのか、非常に不明確な状態が続いている。最終的には、Webサイトの運営者が責任を負うべき問題ではあるだろうが、中小零細企業が多いケータイサイトの運営者がそのような問題意識を持っているとは考えにくいし、DNSリバインディングを含む複雑なセキュリティ問題を独自に研究・解決する能力もないだろう。であれば、日本独自の進化を遂げた携帯電話コンテンツのセキュリティ問題に対して、もっと広い枠組みでの検討が行われる必要があるし、そこにもっとも近い位置にいるのが携帯電話事業者であると私は考える。
追記(2010/2/16)
括弧内で「私も同意」と書いた部分について指摘を頂戴しました。
ここは同意しちゃだめ。Webアプリ側は「対策可能」なのであって、元からそこに問題があるわけじゃない。
[HiromitsuTakagiのブックマーク / 2010年2月15日 (3)より引用]
少なくともWebアプリケーション側の「不具合」ではないように思いますし、実際のところはこんな感じではないでしょうか。
- iモードブラウザ2.0対応のdocomo端末は、DNS Rebindingの攻撃に対して脆弱である。
- 端末の問題であるため端末側で対応することが望ましいが、名前解決はdocomoのゲートウェイ側で行われる場合があり、端末側では対応が難しい。
- docomoのゲートウェイの問題についてはdocomo側で対応することが望ましいが、問題の性質上、対応が難しい (参考: Re:「docomoケータイのDNS Rebinding問題、全国紙で報道」)。
- これらの問題に対してはWebサイト側で対応することが可能であり、しかも簡単に対応できる方法が存在する。
- 従って、Webサイト側での対応が推奨される。
[DNS Rebinding問題の所在 | 水無月ばけらのえび日記より引用]
まことに指摘の通りで、私の本意は、ばけらさんがわかりやすく要約していただいたとおりです。
したがって、携帯電話事業者はこの問題に対して免責されるわけではなく実施可能な対策はとるべきであり、具体的には、iモードIDを用いた「かんたんログイン」のDNS Rebinding脆弱性に書いたように、DNSキャッシュの最短TTLを長くする程度の対策はとるべきだと考えます。
追記(2010/2/22)
かんたんログインのDNS Rebinding脆弱性の実例として、「ケータイtwitter(twtr.jp)においてDNS Rebinding攻撃に対する脆弱性を発見・通報し、即座に修正された」を書きましたのでご参照ください。
2010年01月18日
_iモードブラウザ2.0のXMLHttpRequestでPOSTデータの扱いが困難になった
このエントリでは、iモードブラウザ2.0の制限により、XMLHttpRequestでPOSTメソッドの利用が困難になっていることを確認したので報告する。
iモードブラウザ2.0のJavaScriptを試していて、POSTメソッドでデータが渡せていないことに気がついた。以下のようなプログラムで検証してみた。
【post.html】 <html> <head> <script> function test() { try { var requester = new XMLHttpRequest(); requester.open('POST', '/dumppost.php', true); requester.onreadystatechange = function() { if (requester.readyState == 4) { onloaded(requester); } }; requester.setRequestHeader("Content-Type" , "application/x-www-form-urlencoded"); requester.send("aaa=bbb&ccc=ddd"); } catch (e) { res = requester.responseText; document.getElementById('result').innerHTML = e.toString(); } } function onloaded(requester) { res = requester.responseText; document.getElementById('result').innerHTML = res; } </script> </head> <body> <input type=button value="go" onclick="test()"> <div id="result"></div> </body> </html> 【dumppost.php】 <?php echo "aaa=" . htmlspecialchars($_POST['aaa'], ENT_QUOTES, 'Shift_JIS') . "<br>"; echo "ccc=" . htmlspecialchars($_POST['ccc'], ENT_QUOTES, 'Shift_JIS') . "<br>"; ?>
実行結果は、以下のようになった。まずはChromeのものだが、IEやFirefoxでも同等の結果だ。
次に、ドコモP-07Aによる結果
ドコモの場合を検証するために、Webサーバーに来ているリクエストをキャプチャしてみた。
POST /dumppost.php HTTP/1.1 X-UE-Version: 1 Host: XXXXXXXXXXXXX User-Agent: DoCoMo/2.0 P07A3(c500;TB;W24H15) Content-Type: text/xml Content-Length: 15 aaa=bbb&ccc=ddd
ご覧のようにPOSTデータそのものは送信されてきているが、Content-Typeがtext/xmlになっているために、Webアプリケーション側で受け取れないようだ。
JavaScript側では、この値をapplication/x-www-form-urlencodedに変更しているが、P-07AでもJavaScriptが再開され、あらたな制限がみつかったで報告したように、setRequestHeaderが無効化されているために、この設定が無視されていることが原因のようだ。
これは明らかに、setRequestHeader無効化の副作用であるが、その代償は大きいように思う。PHP以外に、ASP/ASPX、J2EE(JSP)にこのデータを入力してみたが、いずれも値を読み取ることはできなかった。一方、PerlのCGIモジュールでは、POSTDATAという名称のデータとして、POSTデータ全体を読み取ることができた。Perl以外の場合でも、Webサーバーにデータ自体は到達しているのだから値を利用する手段はあるかもしれないが、私が調べた範囲では分からなかった。
上記の結果として、AjaxでPOSTメソッドを扱うには、標準的でない方法を用いる必要があるわけだが、その方法を検討してみた。
- POSTをあきらめてGETを使う
- Perlのように、text/xml形式のデータを読み出せる言語を選択する
- DOMにより、IFRAME内にFORMを作成してSUBMITする
このうち、上記1.については、セキュリティ上の問題が発生し得る。Ajaxのセキュリティ対策として、「GETメソッドを拒絶する」という方法があるからだ。具体的には、SCRIPT要素を使ってSame Origin Policyを回避してAjaxデータを読み出す手法に対抗して、SCRIPT要素では必ずGETメソッドになることから、POSTのみを許容することで対策するという方法だ。既存のアプリケーションがこのような手法によりセキュリティ対策されている場合に、安易にGETメソッドを許容してしまうと、セキュリティホールが混入することになりかねない。
3.については、私が試した範囲ではうまくいっていない。JavaScriptからIFRAME内のFORM操作がうまくいかないのだ。これも、ひょっとすると制限を掛けているのかもしれない。アドホックな方法ならありそうだが、まだ十分に検証できていない。
一つの疑問は、このような情報がインターネット上に見あたらないことだ。AjaxでPOSTメソッドが使えないというのはとんでもないことだが、検索してもそのような情報がみあたらないのだ。iモードブラウザ2.0のAjaxを誰も使っていないのか、それとも事業者が恐ろしくて口をつぐんでいるのか。
しかし、このような技術情報が流通していかない限り、iモードブラウザ2.0のJavaScriptを使ったコンテンツは普及していかないだろう。NTTドコモにはさらなる情報開示を期待したいし、ケータイWebの開発者にも、もっとブログなどでの情報公開を期待する。
2009年10月19日 [Perl][PHP][SQL]
_quoteメソッドの数値データ対応を検証する
このエントリでは、PerlのDBI、PHPのPDO、MDB2にて用意されているquoteメソッドが数値データをどのように扱えるかを検証しました。結論としてMDB2が合格、それ以外は不合格で、とくにDBD::mysqlを使用した場合、脆弱性といってもよいような結果となりました。
概要
DBI、PDO、MDB2は、いずれもデータベースアクセスを抽象化したモジュール(クラス)であり、汎用的な記述によりさまざまなデータベースを利用できるように工夫されています。これらモジュール(クラス)にはquoteというメソッドが用意されています。DBIのquoteメソッドの呼び出し例を示します。
my $dbh = DBI->connect('DBI:mysql:dbname:localhost', 'user', 'pass'); print $dbh->quote("a\\'"); # 「a\'」という文字列を指定 【処理結果】 'a\\\''
ごらんのように、あるいはquoteという名称が示すように、これらメソッドは入力データをSQL文字列リテラルとしてエスケープした上で、シングルクオートでくくります。SQLの文字列リテラルのエスケープ方法はデータベースソフトウェア依存であり、MySQLの場合はシングルクオートとバックスラッシュをエスケープしますが、標準SQLではシングルクオートのみです*1。quoteメソッドを利用することにより、データベースの種類を自動的に考慮して、SQLを安全かつ簡便に動的組み立てできます。
さて、quoteメソッドには省略可能な第二引数があり、データの型を指定できるようになっています(デフォルトは文字列型)。以下に、整数型を指定した場合の呼び出し方を示します。
DBI: $dbh->quote($n, SQL_INTEGER) PDO: $dbh->quote($n, PDO::PARAM_INT) MDB2: $dbh->quote($n, 'integer')
ごらんのように、呼び出し方はほとんど同じです。では、この処理結果は、どうなる *べき* でしょうか。私の過去の日記「数値項目に対するSQLインジェクション対策のまとめ」や「SQLの暗黙の型変換はワナがいっぱい」で説明したように、SQLは本来厳格な型をもった言語であり、数値リテラルをシングルクオートで囲むことは好ましくありません。したがって、quoteメソッドに数値型を指定した場合は、たんなる数値が返るべきだと考えます(SQL組み立てが目的なので、型は文字列型でもよい)。そこで、入力データのサンプルとして「1a\'」を第一引数に、整数型指定を第二引数としてquoteメソッドを呼び出してみました。
DBI: $dbh->quote("1a\\'", SQL_INTEGER) PDO: $dbh->quote("1a\\'", PDO::PARAM_INT) MDB2: $dbh->quote("1a\\'", 'integer')
結果は以下のようになります。PDOとMDB2は、MySQLとPostgreSQLで同じ結果です。
モジュール名 | 結果 |
---|---|
DBI(DBD::mysql) | 1a\' |
DBI(DBD::PgPP) | '1a\\\'' |
PDO | '1a\\\'' |
MDB2 | 1 (int型) |
先に書いた「あるべき結果」になったのはMDB2だけでした。PDOとDBD::PgPPは、文字列型として指定したのと同じ結果であり、SQLインジェクション対策として機能しますが、なんのためにわざわざ整数型を指定したのかわかりません。DBD::mysqlはエスケープもクオートもしていないので、入力として「1 union select table_name from information_schema.tables」を与えられると、一覧表を表示する画面でテーブルの一覧が表示させられるような結果となります。これはまずい。現実問題として、何もしないのでは、わざわざquoteをメソッドを呼ぶ動機がありません。
また、文字列としてエスケープする方法も、先のエントリで説明したように、さまざまな副作用がありますので、quoteメソッドを使うよりは、SQL呼び出し直前でバリデーションするか、sprintfで%dの書式を与えることで、数値のみが出力されるように制限する手法が考えられます。
今回の調査で、MDB2のquoteメソッドは、わりといい線行っていると思いました。MDB2のquoteでは、'decimal'という指定もできるのですが、20桁程度の数値を与えても、破綻なく動作します。
入力 | 出力 |
---|---|
123456789012345678901 (文字列型) | 123456789012345678901 (文字列型、クオートなし) |
1a2b3 (文字列型) | 123 (文字列型、クオートなし) |
'1a2b3'に対する結果が「123」となるところ、「サ、サニタイズかよ」と思ってしまいますが、まぁ数値としてのバリデーションは入り口でやるとして、SQL発行時の「最後の砦」としては、まぁ許容できる範囲ではないかと思います。「例外を発生させるべきじゃないの?」と思うのですが、他のモジュールのだめさ加減との比較では上出来でしょう。
なお、DECIMAL型の場合に、文字列型で結果を返すのは妥当な処理です。PHPに標準で用意されているintegerやfloatでは、DECIMALの桁数(MySQLでは最大65桁、PostgreSQLでは最大1,000桁)を表現するのに十分なビット長がないからです。演算はどうするのだという疑問がありそうですが、DBの性能に余裕があればSQLで演算するという手もありますし、BCMath 任意精度数学関数を使う手もあります。BCMathは文字列型のままで数値演算ができます。
まとめ
DBI、PDO、MDB2のquoteメソッドについて、第二引数明示により数値として処理する内容について検証しました。期待どおり動作するのはMDB2であり、一方DBD::mysqlではなにもしていないので脆弱性対策としては使えないことがわかりました。
また、今回のエントリでは示していませんが、PDOは処理が高速なために人気があるものの、内部処理をつっこんで見ていくと、不安が出てきました。その内容については別途何かの形で発表したいと思いますが、現時点では、id:moriyoshiさんの以下のエントリが参考になると思います。
*1 詳細については、「 SQLのエスケープ再考 」および「SQLエスケープにおける「\」の取り扱い 」に詳しく書きましたので参考になさってください
2009年10月14日 [php]
_htmlspecialchars/htmlentitiesはBMP外の文字を正しく扱えない
PHPの安定版(PHP5.3.0、PHP5.2.11)のhtmlspecialcharsおよびhtmlentitiesには、Unicodeの基本多言語面 (BMP)範囲外の文字、すなわち、U+10000以降の文字を正しく扱えない問題があります。
もっともシンプルな再現コードを以下に示します。
<?php $c = "\xF0\x90\x80\xBC"; // U+1003C $a = htmlspecialchars($c, ENT_QUOTES, 'UTF-8'); echo bin2hex($a) . ':' . $a; 【処理結果】 266c743b:<
U+1003Cは、Wikipediaの説明によると、大昔のギリシャの「線文字B」を表すそうで、小なり記号とは関係ないので、本来そのまま出力しなければならないものです。htmlspecialcharsおよびhtmlentitiesの内部処理で、コードポイントの下位16ビットしかみていないようで、このような結果となります。
線文字Bを扱う人口は少ないと思われますので、もう少し身近な例を探してみました。
<?php $c = "\xF0\xA2\x89\xBF"; // U+2227F(𢉿 …マダレに馬という字) $a = htmlentities($c, ENT_QUOTES, 'UTF-8'); echo bin2hex($a) . ':' . $a; 【処理結果】 26736373696d3b:≿
𢉿は、京都府長岡京市の地名で𢉿子ケ岳(からねがたけ)に使われている文字です(参考:稀少地名漢字リスト、Google Map)。一方、≿はSGMLの文字実体参照のマッピングに出てくる記号で、≿(カーブした大なり記号の下に~)を表します。この実体参照形式はFireFoxなどのブラウザでは表示できませんが、そのような文字実体参照に変換される経緯は、id:moriyoshiさんの「PHPのhtmlentities()で (HTML4.0的に) 余計に実体参照に変換されてしまう文字の一覧」に説明されています。
BMP外の文字を扱う機会は少ないとは思いますが、正常系のデータが正しく扱えないという意味では、先の文字エンコーディングのチェック不備よりも重い問題だと考えます。幸い、id:moriyoshiさんが先に気づかれて、文字エンコーディングの問題と合わせて修正されていますので、おそらくPHP5.3.2からは修正されるものと思われます。PHPの最新のスナップショットにて修正ずみであることを確認しています。
影響を受けるケース
この問題を受けるのは、文字エンコーディングとしてUTF-8を使用している場合に、BMP範囲外の文字が与えられた場合です。
対策
PHP側の対応が完了するまでの間は以下のようにすればよいと思います。htmlentitiesよりはhtmlspecialcharsの方が影響が少ないこと、通常htmlentitiesを使う理由はない(参考:htmlspecialcharsと不正な文字の話 )ことから、htmlspecialcharsを使った上で、影響のある文字を扱わなければならない場合は個別に手当するしかないでしょう。Unicode5.1の範囲で、htmlspecialcharsにより不正に変換される文字は、以下の13種です。htmlentitiesを使用すると616種に増えます。𢉿もその一つです。
U+10022 線文字B音節文字 U+10026 線文字B音節文字 U+1003C 線文字B音節文字 U+20022 𠀢 U+20026 𠀦 U+20027 𠀧 U+2003C 𠀼 U+2003E 𠀾 U+E0022 言語タグ U+E0026 言語タグ U+E0027 言語タグ U+E003C 言語タグ U+E003E 言語タグ
個別に対応する方法を考えてみましたが、簡単な方法は思いつきません。いったん他の文字列に置き換えておいて、htmlspecialcharsの処理結果から、元の文字に戻す方法があると思いますが、面倒な処理になります。あるいは、影響を受ける文字を数値文字参照(𠀢など)に変換しておいて、htmlspecialcharsの第四パラメータ$double_encodeを0にして実行する方法もありますが、$double_encodeを0にする副作用もあります。元々実体参照や数値文字参照の形になってる文字列を変換しなくなるからです。このため現状では上記問題を許容した上で、PHP側で対応されるのを待った方がよい場合が多いような気がします。
まとめ
PHPのhtmlspecialcharsおよびhtmlentitiesには、Unicodeの基本多言語面 (BMP)範囲外の文字を正しく扱えません。次バージョン(PHP5.2.12、PHP5.3.2)では修正されると思われます。それまでの間は以下の方法で対処可能です。
- htmlentitiesではなくhtmlspecialcharsを使用する
- htmlspecialcharsでも影響を受ける13文字は個別に対応する、あるいは許容する
[PR]小規模ECサイトに最適なWAF、SiteGuard Lite
HASHコンサルティング株式会社
最近の記事
- 2011年08月30日
- 1. RSSフィードをリダイレクトします
- 2011年07月01日
- 1.
- 2011年03月29日
- 1. PDO/MySQL(Windows版)の文字エンコーディング指定の不具合原因
- 2011年03月22日
- 1. PHP5.3.6からPDOの文字エンコーディング指定が可能となったがWindows版では不具合(脆弱性)あり
- 2011年01月27日
- 1. CSRF対策のトークンをワンタイムにしたら意図に反して脆弱になった実装例
- 2011年01月04日
- 1. escapeshellcmdの危険な実例
- 2011年01月01日
- 1. PHPのescapeshellcmdの危険性
- 2010年10月03日
- 1. 問題点の概要
- 2010年09月27日
- 1. 文字コードに起因する脆弱性を防ぐ「やや安全な」php.ini設定
- 2010年07月25日
- 1. ツッコミSPAM対策で、ツッコミ抜きのRSSフィードを用意しました
- 2010年07月01日
- 1. ぼくがPDOを採用しなかったわけ(Shift_JISによるSQLインジェクション)
- 2010年04月06日
- 1. PROXY(プロキシ)経由でのDNSリバインディングと対策
- 2010年04月05日
- 1. JavaアプレットのDNSリバインディングはJRE側で対策済みだった
- 2010年03月29日
- 1. DNSリバインディングによる無線LANパスフレーズの読み出しに成功
- 2010年03月25日
- 1. DNSリバインディングによるルータへの侵入実験
- 2010年02月22日
- 1. ケータイtwitter(twtr.jp)においてDNS Rebinding攻撃に対する脆弱性を発見・通報し、即座に修正された
- 2010年02月12日
- 1. かんたんログイン手法の脆弱性に対する責任は誰にあるのか
- 2010年01月18日
- 1. iモードブラウザ2.0のXMLHttpRequestでPOSTデータの扱いが困難になった
- 2009年10月19日
- 1. quoteメソッドの数値データ対応を検証する
- 2009年10月14日
- 1. htmlspecialchars/htmlentitiesはBMP外の文字を正しく扱えない
- 2009年10月09日
- 1. htmlspecialcharsのShift_JISチェック漏れによるXSS回避策
- 2009年09月30日
- 1. htmlspecialcharsは不正な文字エンコーディングをどこまでチェックするか
- 2009年09月24日
- 1. SQLの暗黙の型変換はワナがいっぱい
- 2009年09月18日
- 1. 文字エンコーディングバリデーションは自動化が望ましい
- 2009年09月14日
- 1. 既にあたり前になりつつある文字エンコーディングバリデーション
- 2009年08月05日
- 1. 携帯JavaScriptとXSSの組み合わせによる「かんたんログイン」なりすましの可能性
- 2009年03月28日
- 1. IPAは脆弱性の呼び方を統一して欲しい
- 2009年03月27日
- 2009年03月11日
- 1. U+00A5を用いたXSSの可能性
- 2008年12月22日
- 1. JavaとMySQLの組み合わせでUnicodeのU+00A5を用いたSQLインジェクションの可能性
Before...
_ bero [> PDO+MySQLの場合、プレースホルダは「動的プレースホルダ」あるいは、俗に「クライアントサイドのプリペアード..]
_ bero [上記mysql->libmysql ドキュメントにも書いてた http://php.net/manual/ja/r..]
_ carrot [PHP5.2.1から、PDO_MYSQLは、デフォルトではクライアントサイドのプリペアードステートメントになったよう..]