2009-06-21
■[セキュリティ]XMLをparseするアプリのセキュリティ

「XML」「セキュリティ」という単語でWeb検索すると、多くヒットするのはXMLデジタル署名やXML暗号などを説明したWebページです。
本日の日記では、それとはちょっと違うテーマ(XXEと呼ばれる攻撃)について書きます。
脆弱なコードと攻撃方法
さっそく脆弱性があるサンプルプログラムです。
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; import org.w3c.dom.*; import org.apache.xerces.parsers.*; import org.xml.sax.*; public class Test1 extends HttpServlet { public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { DOMParser parser = new DOMParser(); parser.parse(new InputSource(request.getInputStream())); Document doc = parser.getDocument(); String data1 = doc.getElementsByTagName("data1") .item(0).getTextContent(); String data2 = doc.getElementsByTagName("data2") .item(0).getTextContent(); response.setContentType("text/xml; charset=UTF-8"); PrintWriter out = response.getWriter(); out.println("<result>" + x(data1 + data2) + "</result>"); } catch (Exception e) { e.printStackTrace(); } } private static String x(String s) { s = s.replaceAll("&", "&"); s = s.replaceAll("<", "<"); return s; } }
見ての通り、JavaのServletプログラムです。
リクエストボディーのXMLをparseして、data1要素とdata2要素の中身の文字列を結合して、XMLをレスポンスする単純な処理をしています。
たとえば、このプログラムに、以下のようなXMLリクエストを与えます。
■リクエストA <?xml version="1.0"?> <str><data1>xxx</data1><data2>yyy</data2></str>
それに対して、以下のようなレスポンスが返ります。
■レスポンスA <result>xxxyyy</result>
ある意味で何の変哲もないプログラムですが、このプログラムにはセキュリティ上の問題があります。
たとえば、以下のような文書型宣言と外部実体参照を含むXMLを送ります。
■リクエストB <?xml version="1.0"?> <!DOCTYPE str [ <!ENTITY pass SYSTEM "/etc/passwd"> ]> <str><data1>&pass;</data1><data2></data2></str>
それに対して、以下のようなレスポンスが返ります。
■レスポンスB <result>root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin adm:x:3:4:adm:/var/adm:/sbin/nologin lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin sync:x:5:0:sync:/sbin:/bin/sync (省略) </result>
サーバ上の/etc/passwdファイルの中身が、レスポンスに出力されてしまいました。
このような現象は、Javaだけで起こるものではありません。下のプログラムは、さきほどのJavaプログラムと同様の処理をするPHPのプログラムです。
<?php header('Content-Type: text/xml'); $doc = new DOMDocument(); $doc->loadXML(file_get_contents('php://input')); $data1 = $doc->getElementsByTagName('data1')->item(0)->textContent; $data2 = $doc->getElementsByTagName('data2')->item(0)->textContent; echo "<str>". htmlspecialchars($data1. $data2). "</str>";
上のPHPのプログラムに対して、上のリクエストBを送ると、Javaと同じようにサーバ上の/etc/passwdファイルの中身がレスポンスされます。
どのようなファイルが漏洩するか
今見たように、上のJava(あるいはPHP)のプログラムでは、サーバ上のファイルの中身が漏洩します。
しかし、サーバ上のすべてのファイルが漏洩するわけではありません。漏洩するのは、サーバアプリケーションの実行ユーザが読み取り権限を持つファイルで、かつ中身が「外部解析対象実体」(External Parsed Entity)に適合するファイルのみです。たとえば、「123&456」や「123<>456」のような内容を持つファイルは、読み取ることはできません。また、XML文書を読み取ると、そのtextContentだけが応答されます。
また、ファイルの中身を盗み出せるのは、XMLの要素の内容をアプリケーションが何らかの形で外部に送り返す場合に限られます。それでは、要素の内容を送り返さないアプリケーションは安全なのかというと、そうでもありません。ファイルの中身は盗み出せなくても、エラーメッセージや応答時間から、特定のファイルがサーバ上に存在するか/しないかの情報を外部から得られる場合があります。
それ以外の攻撃
攻撃の被害は、ファイルの中身が盗み見られるだけではありません。以下のようなXMLを送ることで、本来は外部からアクセスできない情報を盗み出すことができるかもしれません。
<?xml version="1.0"?> <!DOCTYPE str [ <!ENTITY mysecret SYSTEM "http://internalhost/secret.txt"> ]> <str><data1>&mysecret;</data1><data2></data2></str>
同じ手法で、サーバ側の内部ネットワーク上のホストに対して、特定のポートが開いているか/閉じているかを、エラーメッセージや応答時間から調べられる場合があります。つまり、内部ネットワークのポートスキャンに利用できるわけです。
下のページには、サーバ上のファイル漏洩やポートスキャンのほかに、DoS攻撃に利用されうることなどが書かれています。
(参考)SecuriTeam - XXE (Xml eXternal Entity) Attack
このアドバイザリ(おそらく、この種の攻撃に関する最初のアドバイザリ)では、外部実体参照を利用した攻撃を、XXE(XML eXternal Entity)Attackと呼んでいます。
また、ちょっと毛色が違う攻撃として、入れ子の内部実体参照を利用したDoS攻撃があることが知られています。
(参考)デベロッパーズコーナー:Javaプログラミングを極める「DoS攻撃への対策」(1) - XML Square
入れ子の実体参照を使う手法は、昨年RubyのREXMLで脆弱性が見つかったため、ご存知の方もいるかと思います。
対策
いくつかの対策方法について書きます。基本的に、Java+Xerces2環境で、DOMを使うプログラミングをすることを前提としています。
外部実体を使用禁止にする
上で紹介したXXE攻撃に関するページで、推奨されている対策です。
Suggested fix:
Most XML parsers allow their user to explicitly specify external entity handler. In case of untrusted XML input it is best to prohibit all external general entities.
SecuriTeam - XXE (Xml eXternal Entity) Attack
Xerces2では、以下のようにsetFeatureすることで、外部一般実体(External General Entity)を無効にできます。
DOMParser parser = new DOMParser(); parser.setFeature("http://xml.org/sax/features/external-general-entities", false); …
しかし、外部実体を禁止する対策は、ファイルの存在やポートの開閉状態を調べる攻撃に対しては無力な場合があります。例えば、以下のように文書型宣言で外部のDTD(外部サブセット)を読み込むことで、ポートの開閉を調べる攻撃方法が考えられます。
<!DOCTYPE str SYSTEM "http://internalhost:1234/">
また、外部のDTDを呼び出す場合、DTD内にパラメータ実体を定義・参照する方法により、ファイルの存在などを調べることも可能です。
外部のDTDを読み込まないようにするためには、以下の設定が必要です。
parser.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
DTDを使用禁止する
DTDを使用禁止にする(そのために文書型宣言を禁止する)ためには、以下のようにsetFeatureします。おそらく、これが一番確実な方法です。
DOMParser parser = new DOMParser(); parser.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); …
なお、これまで紹介した対策のコードでは、DOMParserクラスのsetFeatureというメソッドを使いました。Xerces2で利用できるfeatureの一覧は、以下のページに書かれています。
(参考)Features
その他の対策方法
根本的な話として、まったく独自のXMLをやりとりするのではなく、SOAPやXML-RPCにのっとった電文形式として、それ用のライブラリ(実績がありそうなもの)を用いてparseする方法もあります。
SOAPに関しては、メッセージ中に文書型宣言を含めることが仕様で禁止されています。
The XML infoset of a SOAP message MUST NOT contain a document type declaration information item.
SOAP Version 1.2 Part 1: Messaging Framework (Second Edition)
上の文章は、基本的にはメッセージの送信者を規制するものだと受け取れますが、受信者が文書型宣言を含むメッセージを受け取った時には、エラーとするのが自然だろうと思います。
実際に、Java、PHP、ASP.NET用のいくつかのSOAPライブラリの挙動を調べたところ、多くのライブラリは文書型宣言を受け付けないように実装されていました。
たとえば、ASP.NET環境のWebサービスに、文書型宣言を含むメッセージを送ると400(Bad Request)応答が返ります。JavaのAXISでは、500(Internal Server Error)応答が返ります。PHPのPEAR::SOAPライブラリでは、文書型宣言が含まれていてもエラーにはなりませんが、外部のDTDの読み込みや、外部実体の読み込みは行われません。
一方で、XML-RPCの仕様にはDTDや外部実体に関する規定はありません。XML-RPCのライブラリを使う場合は、事前に実装を確認した方がよいと思います。
PHPの場合
PHP5のDOMを使う場合、parse時のオプション(libxmlに渡すオプション)がいくつかあります。また、DOMDocumentクラスには、処理に影響するようなプロパティが定義されています。
(参考)
外部のDTDの読み込みは、デフォルトでは無効になっており、オプションで有効にするようになっています。
外部実体については、DOMDocumentクラスにresolveExternalsというプロパティがあって、それを使うことで制御できると思ったのですが、調べてみると違う用途のものだとわかりました。他のプロパティやオプションについて、実際のプログラムで動作を確認したり、ネットで情報を調べたりもしましたが、外部実体やDTDを禁止する方法は結局判りませんでした。*1
もし、parser側に外部実体やDTDを禁止する設定がないならば、アプリケーション側で対策をせざるをえません。これは非常に厄介です。
私が思いつくのは、parseした後に、ルートノードから子ノードのnodeTypeを再帰的に調べていき、文書型宣言などのノードが含まれているかを確認する方法です。もし、排除したいタイプのノードが含まれている場合にはエラーとします。
しかしこの方法は、サーバ上のファイルの存在を外部から調べたり、内部ネットワークをポートスキャンする攻撃に対しては、完全な防御とはなりません。なぜなら、XMLをparseしてしまうと、その時点で外部実体が読み込まれるからです。
そうであれば、parseする前に、XMLのバイト列を検索して「<!DOCTYPE」などを探し出す方法も思いつきます。しかし、この方法だとUTF-7やUTF-16などの文字コードを使うことでフィルタを回避できてしまいます。
もし本当に、外部実体やDTDを禁止する方法がDOM関数にないのならば、信頼しないXMLをparseするときには、DOM以外の別の種類のparserを使用する方がよいでしょう。PHPには、PHP: XML 操作 - Manualにあるように、何種類かのXML parserが用意されています。実際、先に触れたPEAR::SOAPライブラリでは、DOMではなく、昔ながらのXML parser(PHP: XML パーサ - Manual)を使用しています。
脆弱なアプリケーションはどれくらいあるのか
上で紹介した、XXE攻撃の最初のアドバイザリが書かれたのは2002年ですので、かなり昔から知られていた攻撃だといえます。しかし、XXE攻撃に関するネット上の情報(特に日本語の情報)は少ないです。この日記の冒頭に書いたように、「XML セキュリティ」のようなキーワードで検索しても、開発者向けにXXE攻撃を説明した情報はなかなか見つけられないのです。
私自身も、Webセキュリティの本(Hacking Exposed Web Applications, Second Edition: Web Application Security Secrets and Solutions)を読むまでは、XXE攻撃についてまったく知りませんでした。開発者の中にも、XMLを扱う際にセキュリティ上考慮すべき事があることを知らない人は多いのではないかと思います。
私自身の経験で言えば、XMLを取り扱うアプリケーションを検査することは少ないですが、サーバ側のファイルの内容が盗み出せたり、ファイルの存在を調べることができる問題を持つアプリケーションをいくつか見たことがあります。近年は、XMLのAPIを公開したり、利用したりするサイトが増えてきているようですので、この種の脆弱性を持つアプリケーションが広く存在している可能性があります。
オープンソースのPHPアプリケーションについても少し調べたところ、サーバ側のファイルの内容が盗み出せるものもありましたが、著名なソフトに関しては、それほど多くはないようです*2。実のところ、サーバ上のファイルの内容が盗み出せるかは、アプリケーションの仕様だけではなくコーディングの仕方にも依存します。
下のPHPのプログラムは、test要素のテキストを、2つの異なる方法で取り出しています。
<?php ... $elm = $doc->getElementsByTagName('test')->item(0); // A: 外部実体参照が展開される $var = $elm->textContent; // B: 外部実体参照は展開されない $var = $elm->firstChild->nodeValue;
Aでは外部実体の中身を含む値が$varに代入されます。一方、Bでは外部実体の中身は$varに代入されません。
私が見たアプリケーションの半数以上は、Bのスタイルのコーディングがされていました*3。セキュリティを意図したものかは不明ですが、結果的にはファイルを盗み取れないようになっていました。
その他のXMLの機能
XMLで外部のファイルを読み込ませる方法は、既に説明したように、外部のDTDや外部実体を利用する方法があります。それ以外にも、XSLやXIncludeには外部のファイルを読み込む方法が用意されています。これらが問題になるケースは、外部実体よりも少ないと思いますが、使用する際には注意が必要です。
■[セキュリティ]IE8のtoStaticHTML関数

以前の日記で、IE8β2のtoStaticHTML関数にバグがあると書きました。
そのバグについては、発見したときにMSに報告しました。その後、特に「直した」という連絡はありませんが、IE8の正式版では修正されていました。
β2にあったバグのPOCは、以下のようなものです。
<body> <div id="foo"></div> <script> var tmp="<img style=\"color:expression(alert('; width:x'))\">"; document.getElementById('foo').innerHTML = toStaticHTML(tmp); </script> </body>
ポイントは、alertのちょっとうしろに入れたセミコロンです。β2では、セミコロンが含まれていると、そこで1つの宣言が終わると解釈していました。かなり荒っぽい解釈です。
IE8正式版で上記のHTMLを表示すると、style属性の中身はざっくり空っぽにされます。つまり何らかの対策がされたということだと思います。
そのほかのパターンも試して見ましたが、CSSに関しては非常に大胆な処理がされます。具体的にいうと、括弧がCSSに含まれていると、それだけではじいてしまいます。
ですので、以下のHTMLはJavaScriptを含んでいないのにも関わらず、toStaticHTMLを通りません。
<b style="background-image: url('http://...');">111</b> <b style="font-family: 'foo(';">111</b>
括弧「(」は、「(」や「\0028」や「\28」にしても、とにかく通してくれません。他にも「\26#x28;」などいろいろな手を使ってみましたが、文字参照まわりは割とまじめに正規化するようになっているため、「(」を入れることができません。なお、括弧が駄目なのは、style属性だけでなくstyleタグも同じです。
攻撃を防ぐためにはこれでよいのでしょうけども、実用上の観点でいうと「(」が使えないのは困ることがあると思います。
その他にも気になることがいくつかあります。
■入力A <style>ul > li {color:red;}</style> ■出力A (HTMLエンコードされる) <style> ul > li {color:red;} </style> --------------------------------------------------- ■入力B <style>li {font-family:'xxx\27\7Dyyy';}</style> ■出力B (CSSの「\」エンコードがデコードされる) <style> li {font-family:'xxx'}yyy';} </style> --------------------------------------------------- ■入力C <style>li {font-family:'xxx\yyy';}</style> ■出力C (なぜかNULL文字が出てくる) <style> li {font-family:'xxx�yyy';} </style>
上のBはXSSまであと一歩という感じですが、結局括弧が入らないために、どうにもなりません。
ただし方向性を少し変えると、うまいことXSSできる場合があります。また、一定の条件がそろえば、XSSできるような軽微な問題も残っています(いずれも互換モードの場合のみ)。CSS周りは、たいていのXSSフィルタにとって最大の鬼門ですが、toStaticHTMLも例外ではないようです。