トップ 追記
過去の日記

2010-10-03 2010-10-03 「PHPで作成する携帯会員サイトの基本」の諸問題(1)

問題点の概要

CodeZineから発表されている「PHPで作成する携帯会員サイトの基本」という記事はツッコミどころ満載で、既にいくつかの問題が修正されているのだが、まだ残っている問題があることや、修正内容にも疑問があるので、いくつか指摘してみたい。ざっと書いたところ、ものすごく長くなりそうだったので、小出しで「連載」の形で書く。忙しいので途中でやめるかもしれない。今回は、問題点の概要を報告する。

くだんの記事をざっと見たところ、以下の問題を見つけた。

IPアドレス制限のない「かんたんログイン」

Net_UserAgent_Mobileを用いて携帯電話の端末IDを取り出し、かんたんログインを実装しているが、ゲートウェイのIPアドレス経由であることを確認していない。以下のリストは、端末IDを取り出しているところ(4ページ目)。

$agent = Net_UserAgent_Mobile::singleton();
$uid = $agent->getUID();

かんたんログインは、IPアドレスチェックをしても安全とはいえないが、IPアドレスチェックをしていないのは論外といえる。

参考→「高木浩光@自宅の日記 - はてなのかんたんログインがオッピロゲだった件

クロスサイト・スクリプティング(XSS)

プロフィール画面にて、氏名の表示箇所にクロスサイト・スクリプティング脆弱性がある(4ページ目)。以下のリストが該当の表示部分だが、登録・更新の画面でも文字種などはチェックされていない。加えて、データベースには、どの項目も255文字までの登録が可能なので、攻撃には十分な文字数である。

<?php $userInfo = $ss->getSession(); ?>
ようこそ、<?php echo $userInfo['user_name']; ?>さん    <br /> 

このページはユーザプロファイルの表示画面だから、本来は、自分自身のユーザ情報のみが表示される。この制約下で、どこまでXSS攻撃が可能かどうかを考えることは、脆弱性のリスクを分析するトレーニングとして好適だろう。ヒントとしては、このサンプルスクリプトは、URLにセッションIDを埋め込んでいることと、かんたんログインをサポートしていることである。

無意味なセッションIDの再生成

ページ生成毎にHTTP_Session2::regenerateId(true);を呼び出している。すなわち、ワンタイムのセッションIDを採用している。脆弱性ではないが、以下の欠点があり、使い物にならないだろう。

  • ブラウザ機能による「戻る」操作ができない
  • リロードするとセッションが切れる(ログアウト状態になる)
  • 電波状態などの理由で一度でも通信が途切れるとセッションが切れる

最初の稿では、regenerateId()の引数trueが省略されていたので、上記欠点がない代わりに、セキュリティ上はなんの効果もなく、逆にログアウト処理ができないという副作用があった。

MySQLの設定によってはSQLインジェクション脆弱性

最初の稿では、Shift_JIS文字エンコーディングでPDOを使っているため、SQLインジェクション脆弱性があった(参考→「ぼくがPDOを採用しなかったわけ(Shift_JISによるSQLインジェクション) 」)。現在は改修されているが、それでもPHPの内部文字エンコーディングをShift_JISにしている点はそのままで、好ましくない。

元パスワードを確認しないパスワード変更機能

これは見出しの通り

パスワードの保存方法

元々は平文でパスワードを保存していたが、その後改修され、MD5ハッシュの形で保存されている。要件によってはパスワードの平文保存が絶対ダメとまでは思わないが、仮にハッシュで保存するのであれば、saltなしのMD5ハッシュでは意味がない。これについては後日詳しく書く予定。

まとめ

PHPで作成する携帯会員サイトの基本 」という記事について、主にセキュリティ上の問題点の概要を説明した。現在は改修されているものもあるが、SQLインジェクションあり、XSSあり、IPアドレス制限すらないオッピロゲのかんたんログインありで、脆弱性のオンパレードだ。SQLインジェクションは改修されたが、他の脆弱性は現時点で改修されていないようだ。読者は、絶対に参考にしてはならないし、ましてやコピペして本番サイト開発に利用してはならない。もっとも、regenerateId(true)の副作用が強すぎて、仮にコピペしても使いものにならないと思われる。

本日のツッコミ(全4件) [ツッコミを入れる]

Before...

roo [> 仮にハッシュで保存するのであれば、saltなしのMD5ハッシュでは意味がない。これについては後日詳しく書く予定。..]

徳丸浩 [rooさん、コメントありがとうございます。 そうですね、僕も書きたいと思っているのですが、ちょっと時間がとれません。..]

roo [お返事ありがとうございます。 講演資料、大変参考になりました。 saltなしハッシュの危険性がわかりました。 そうい..]


2010-09-27 2010-09-27

文字コードに起因する脆弱性を防ぐ「やや安全な」php.ini設定

PHPカンファレンス2010にて「文字コードに起因する脆弱性とその対策」というタイトルで喋らせていただきました。プレゼンテーション資料をPDF形式slideshare.netで公開しています。

文字コードのセキュリティというと、ややこしいイメージが強くて、スピーカーの前夜祭でも「聴衆の半分は置いてきぼりになるかもね」みたいな話をしていたのですが、意外にも「分かりやすかった」等の好意的な反応をtwitter等でいただき、驚くと共に喜んでいます。土曜にPHPカンファレンスに来られるような方は意識が高いというのもあるのでしょうね。

さて、その場で少し触れた「文字コードをやや安全に扱う方法」を紹介したいと思います。

実はこの方法を紹介するかどうかは悩みどころでして、完全に安全になるならともかく、「これで全部おk」みたいに開発者が文字コードのセキュリティに無関心になる方法に働くといやだなと思うわけです。しかし、ネットには、既に根拠の不明確なバッドノウハウであふれているわけで、少しマシなバッドノウハウを流通させることで、改善になるのではないかと思い、公開に踏み切りました。しかし、専門家として、どこまで安全にできるかとか、安全になる根拠は示したいと思います。

文字コードをやや安全に扱うphp.ini設定

まずは結論を示します。php.iniに(あるいは.htaccessで)以下を設定します。

;; 出力バッファリングを無効にする (追記:文字エンコーディングの変換をしなければ、On でもいいです)
output_buffering      = Off

;; HTTPレスポンスの文字エンコーディングを設定
default_charset       = UTF-8

;; デフォルトの言語を日本語にする
mbstring.language = Japanese

;; HTTP 入力変換を有効にする
mbstring.encoding_translation = On

;; HTTP 入力エンコーディング変換を UTF-8 に設定(UTF-8→UTF-8の変換)
mbstring.http_input   = UTF-8

;; HTTPレスポンスは変換しない
mbstring.http_output  = pass

;; 内部エンコーディングを UTF-8 に設定
mbstring.internal_encoding = UTF-8    

;; 無効な文字は「?」に
mbstring.substitute_character = "?"

php.iniで指定できない条件として以下も必須です。

htmlspecialcharsの第3引数に必ず'UTF-8'を指定する

PHPのソースはUTF-8でセーブする

データベースの接続・テーブル・データベースの文字エンコーディングは全てUTF-8で統一する

この設定の要点を箇条書きで説明します。

  • Webアプリケーションの入口から出口まですべてUTF-8で統一する
  • 入力時にUTF-8→UTF-8の変換をすることで、不正な文字エンコーディングを除去する
  • HTTPレスポンスの文字エンコーディングをUTF-8と明示する

この設定でなぜ、どこまで安全になるか、PHPカンファレンスで実演した脆弱性デモとの対応で説明します。

文字コードをやや安全に扱うphp.ini設定はなぜ安全か

以下の説明は、先に紹介したプレゼン資料と対比させながらご覧ください。

「デモ1:半端な先行バイトによるXSS」への対応

半端な先行バイトはUTF-8でもあり得ますが、UTF-8→UTF-8変換の過程で半端な先行バイトは除去されます。

「デモ2:UTF-8非最短形式によるパストラバーサル」への対応

非最短形式のUTF-8もUTF-8→UTF-8変換の過程で除去されます

「デモ3:5C問題によるSQLインジェクション」への対応

UTF-8では原理的に5C問題は発生しません

「デモ4:UTF-7によるXSS」への対応

default_charsetをUTF-8に設定することで、HTTPレスポンスヘッダの文字エンコーディングが設定され、UTF-7によるXSSを防げます。

「デモ5:U+00A5によるSQLインジェクション」への対応

実はこれについてはphp.iniだけでは防げませんが、データベース側の設定もUTF-8に統一すれば防げます。

「デモ6:U+00A5によるXSS」への対応

アプリケーションの内部でUTF-8を一貫して使うことにより、文字集合の変更がなくなるので、U+00A5によるXSSは発生しません。

確認方法

PHPカンファレンスでも紹介した「尾骶骨テスト」や「つちよしテスト」が有効です。以下の文字を入力・登録して、どのように表示されるかを調べてください。


  • ¥(U+00A5) バックスラッシュに変換されないか
  • 骶(U+9AB6) JIS X 0208にない文字
  • 𠮷(U+20BB7) BMP外の文字 UTF-8では4バイトになる
  • このテストにより、文字集合の変更がどこかで起こってないかを確認することができます。データベースによってはBMP外の文字(U+10000以降の文字)に対応していないものもあるので、「つちよしテスト」を全てのアプリケーションがパスできるとは限りません。逆に、U+00A5がバックスラッシュ(0x5C)に化けるアプリケーションには潜在的な危険性があります。

    注意事項

    今回紹介したphp.ini設定は、UTF-8→UTF-8の変換以外はバッドノウハウでもなんでもなくて、ごくまともな設定です。問題は、UTF-8→UTF-8の変換に文字エンコーディングの妥当性チェックを頼っているので、サーバーの移設などでphp.iniの設定が変更され、文字エンコーディングの自動変換がされなくなった場合に危険になることです。php.iniが変更された可能性がある場合は、phpinfoなどにより、mbstring.encoding_translationがOnになっていることを確認してください。同様に、default_charsetがUTF-8になっていることを確認してください。

    こういう問題もあるので、本当はスクリプトで、レスポンスヘッダの文字エンコーディング指定や、文字エンコーディングのチェックをした方が頑丈なアプリケーションになります。新規開発の場合は、フレームワークの設定やカスタマイズなどで、これらの処理が行われることを確実にするとよいでしょう。

    また、講演中にhtmlspecialcharsの第3引数(文字エンコーディング指定)を必ず指定するように強調しましたが、それは今回紹介する方法でも変わりません。技術力のある方の中には、「でも入口で不正な文字エンコーディングが除去されているから、htmlspecialcharsの第3引数は指定しなくても一緒でしょ」と言う人もいると思いますが、以下のような実効性もあるのです。

    それは、mbstringではチェックしないが、htmlspecialcharsではチェックするという項目があるのです。UTF-8の5バイト、6バイトの形式です。これを確認するスクリプトを作りました。

    <?php
      $u8_6 = "\xFC\x84\x80\x80\x80\x80"; // 6バイト形式のUTF-8
      var_dump(mb_check_encoding($u8_6, 'UTF-8'));
      echo bin2hex(mb_convert_encoding($u8_6, 'UTF-8', 'UTF-8')) . "\n";
      var_dump(htmlspecialchars($u8_6, ENT_QUOTES, 'UTF-8'));
      echo bin2hex(htmlspecialchars($u8_6, ENT_QUOTES));
    
    【実行結果】
    bool(true)         # checkは通っている
    fc8480808080       # UTF-8→UTF-8の変換後もそのまま
    string(0) ""       # htmlspecialcharsにUTF-8を指定すると削除される
    fc8480808080       # htmlspecialcharsに文字エンコーディングを指定しないと、そのまま
    

    このように、mbstringではUTF-8の5、6バイトの表現を認めていますが、htmlspecialcharsを通すと、5バイト以上の表現は除去されます*1。IEやFirefoxなど、現在広く使用されているブラウザはUTF-8の5、6バイトの表現を認識しないと思いますが、万一、UTF-8の5、6バイトの表現を用いた攻撃手法が見つかっても、htmlspecialcharsが削除してくれると安心です。こういうこともあり得るので、htmlspecialcharsの第3引数は指定するようにしましょう。これも、フレームワークに組み込むか、ラッパー関数を用意しておけば簡便です。

    本当は、htmlspecialcharsのデフォルト文字エンコーディングがmbstring.internal_encodingになってくれると楽ちんなのですが、現在はISO-8859-1がデフォルトなので、こういう面倒なことになります。

    まとめ

    「文字コードをやや安全に扱うphp.ini設定」を紹介し、安全になる根拠および限界を説明しました。「これさえやっておけば、おk」となるのではなく、むしろ文字コードの問題に興味を持っていただくためのきっかけになれば幸いです。なお、文字コードの話は、SQLインジェクションとかXSSなど脆弱性対策をした上での話ですので、php.iniだけで脆弱性対策が不要になるわけではないので、念のため。

    *1 この問題については、id:t_komuraさんの素晴らしいブログエントリ「最新の PHP スナップショットでの htmlspecialchars()/htmlentities() の修正内容について」に詳しい分析があります


    2010-07-25 2010-07-25

    ツッコミSPAM対策で、ツッコミ抜きのRSSフィードを用意しました

    昨日17:47から19:13にかけて、大量のツッコミSPAMが来ています。取り急ぎ、ツッコミの一日あたりの上限数を0にすることにより、対応しましたが、フィードで日記をご覧いただいてる方々には、見苦しいものをお見せして申し訳ございません。

    ツッコミ対策は色々工夫してはおりますが、まだ決定的なものはない状態で、時々このような事態となっております。

    当面の問題として、RSSフィードがSPAMだらけになることが問題ですので、ツッコミを含まないRSSフィードも生成する設定にいたしました。ツッコミがうざいと思われる方は、恐れ入りますが、以下のURLから再設定いただけるとよろしいかと思います。

    http://www.tokumaru.org/d/no_comments.rdf

    no_comments.rdfの方は当面試験運用といたします。不具合などありましたら、ツッコミなどでお知らせください。

    本日のツッコミ(全2件) [ツッコミを入れる]

    YuneKichi [no_comments.rdfに限らないのですが、この記事以降のフィードが更新されていないように思われます。]

    徳丸浩 [YuneKichiさん、ご指摘ありがとうございます。 設定がおかしかったので本日修正しました。ありがとうございまし..]


    2010-07-01 2010-07-01

    ぼくがPDOを採用しなかったわけ(Shift_JISによるSQLインジェクション)

    PHPのデータベース・アクセス・ライブラリPDOは、DB接続時の文字エンコーディング指定ができないため、文字エンコーディングの選択によっては、プレースホルダを使っていてもSQLインジェクション脆弱性が発生します。

    はじめに

    本を書いています。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つの方法です。

    1. PDOではなくMDB2など文字エンコーディングを正しく扱えるライブラリを使用する(根本的対策)
    2. アプリケーションから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。
    
    gist:459499 - GitHubより引用

    ご覧のように、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でも問題ないことになります

    本日のツッコミ(全7件) [ツッコミを入れる]

    Before...

    bero [> PDO+MySQLの場合、プレースホルダは「動的プレースホルダ」あるいは、俗に「クライアントサイドのプリペアード..]

    bero [上記mysql->libmysql ドキュメントにも書いてた http://php.net/manual/ja/r..]

    carrot [PHP5.2.1から、PDO_MYSQLは、デフォルトではクライアントサイドのプリペアードステートメントになったよう..]


    2010-04-06 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 2010-04-05

    JavaアプレットのDNSリバインディングはJRE側で対策済みだった

    Black Hat Japan 2007における金床氏のプレゼンテーション金床本で、Javaアプレットに対するDNSリバインディングの手法が説明されている。ここしばらく、その追試を行っていた。結論としては、金床氏の方法にはJRE側で対策がとられていたので報告する。

    金床氏が報告しているDNSリバインディングの手法とは以下のようなものだ(金床本に説明されている方法)。

    1. 被害者のユーザにワナのページをPROXY経由で閲覧させる
    2. ワナのドメイン(FQDN)に対するIPアドレスを変更する
    3. ワナページ上で数秒〜数十秒のちにJavaScriptによりJavaアプレット用のHTMLをレンダリングする
    4. JREがアプレットをPROXY経由で*1ダウンロードする
    5. アプレットのダウンロード時点で既にDNSレコードは書き換わっているが、PROXYがDNSキャッシュしているため、アプレットは正常にダウンロードされる
    6. アプレットからワナサイトのFQDNの任意のポートに対してSocket通信する。
    7. 既に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のバージョンは最新にすべきだ。

    本日のツッコミ(全3件) [ツッコミを入れる]

    金床 [FYIヽ(´ー`)ノ http://sunsolve.sun.com/search/document.do?asse..]

    金床 [>おそらく金床氏もupdate 3で対策されたとは明確には分からなかったのだろう。 いえいえ、ちゃんとわかってまし..]

    徳丸浩 [金床さん、コメントいただきありがとうございました。 sunのドキュメントはまだきちんと読めていないので、読ませて頂き..]


    トップ 追記