HTML PurifierのSecurity Fix

HTML Purifierの4.1.1がリリースされました。今回のリリースには1件のSecurity Fixが含まれています。今日はその内容について少し書きます。

IECSSのurl()の扱い

以下のようなstyle属性があったとき、ブラウザはどのように解釈するでしょうか?

<span style="background: url('http://host/aaa\'\);color:red;')">111</span>

FirefoxOperaSafariでは、「http://host/aaa');color:red;」というURIをもつbackgroundプロパティと解釈します。したがってcolorプロパティが有効になることはありません。これはCSSの仕様から見ても至極妥当な挙動です。

ところがIEだけが違う解釈をします。IEで上記のHTMLを表示させると、backgroundプロパティのURI値は「http://host/aaa\'\」と解釈されます。そして、その後ろのcolorプロパティが有効となり「111」という文字は赤字で表示されます。

このように、IEはurl()内の文字列リテラルにおいて「\」によるエスケープを解釈しません。HTML Purifierの4.1.1未満にあった脆弱性は、IEのこのような特異な解釈(バグ)を適切にハンドリングできないというものでした。

font-familyプロパティ

font-familyプロパティでも「'」または「"」で括ったリテラルが使用可能です。

こちらはどうなのか以下のHTMLで試してみます。

<span id="s1" style="font-family: 'aaa\';color:red;'">111</span>

<script>
alert(document.getElementById('s1').style.fontFamily);
</script>

このHTMLをIEで表示すると、他のブラウザと同じく「'aaa';color;red;'」がalertされます。つまり、font-familyについていえば、IEも「\」によるエスケープ構文をサポートしているということになります。

ならばurl()でも「\」エスケープをサポートすればよさそうなものですが、上で説明したようにそうはなっていません。ひょっとしたら、url()では「C:\terada\...」のようなパスが使われる可能性を考慮して、「\」エスケープを解釈しないのかも…と推測していますが、真相はわかりません。

とられた対策

HTML Purifier 4.1.1では以下の対策がとられました。

Rewrite CSS url() and font-family output logic.

The new logic is as follows:

  • Given a URL to insert into url(), check that it is properly URL encoded (in particular, a doublequote and backslash never occurs within it) and then place it as url("http://example.com").
  • Given a font name, if it is strictly alphanumeric, it is safe to omit quotes. Otherwise, wrap in double quotes and replace '"' with '\22 ' (note trailing space) and '\' with '\5C ' (ditto).

Public Git Hosting - htmlpurifier.git/commit

実はこの対策は私が提案したものがベースになっていたりします(Release Noteにcreditしてくれました。脆弱性自体はMario Heiderich氏が報告したようです)。

IEの挙動が変わらない限り、このようなちょっと面倒な対処をせざるをえないと思います。

JavaScriptの文字列リテラルでXSS

たまに以下のようにJavaScriptの文字列リテラルに値が入るアプリを見ることがあります。

<script>
var foo="●";
...
</script>

値は「●」の箇所にHTMLエスケープされて出力されます(下の方の例も同じ)。

こんなケースでどうXSSするか?という話です。

簡単にXSSできるケース

以下のパターンだとXSSするのは簡単です。

<script>
var foo="●"; var bar="●"; ...
</script>

?foo=\&bar=-alert(123)//のような値を与えるだけです。

難しいケース

次はこんなパターンを考えます。

<script>
var foo="●";
var bar="●";
...
</script>

こうなると難易度はぐっと上がります。というよりも、ほとんどの場合はXSSできません。

しかし、状況次第ではXSSできることもあります。

攻撃方法

HTMLの文字コードにはUTF-8が指定されているものの、UTF-8として不正なバイトシーケンスがHTMLに出力できる状況であるとします。

そんな状況ならば、?foo=%F0&bar=-alert(123)//のような値を与えることでXSSできます。

%F0(0xF0)はUTF-8の4バイト文字の先頭バイトです。IE6だと%F0の後ろの3バイトを食いつぶしてくれます。JavaScriptコード上で、[0xF0]の後ろに「"」(0x22)、「;」(0x3B)、LF(0x0A)の3バイトがありますが、それらがうまいこと食いつぶされるということになります。

HTMLの改行文字がLFではなくCR LFならば、後ろの4バイトを食いつぶすために、UTF-8の「5バイト文字」を使う必要があります。厳密にいうと「5バイト文字」というのは規格上存在しませんが、IE6には存在するようで、fooに「%F8」を入れれば後ろの4バイトが食いつぶされてうまくXSSできます。

IE6+UTF-8での"食いつぶし"

余談ですがIE6のUTF-8処理はかなりユニークです。

ここでは「©」(U+00A9)という文字をとりあげて説明します。

この文字は、UTF-8エンコードすると[0xC2][0xA9]というバイトになります。これを2進数(ビット)であらわすと、以下のようになります。

0xC2     0xA9
11000010 10101001

UTF-8では2バイト目以降の先頭2ビット(上の赤字部分)は「10」で固定です。固定なので、コードポイントを示すデータではなく、「2バイト目以降である」ことを示す意味しか持っていません。とらえようによってはどうでもいい部分ということです。

IE6のUTF-8デコーダは、この2ビットを無視してデコードします。これを利用すると、ある文字を複数のバイト列で表現することができます。

11000010 00101001 ←0xC2 0x29
11000010 01101001 ←0xC2 0x69
11000010 10101001 ←0xC2 0xA9(ただしいU+00A9)
11000010 11101001 ←0xC2 0xE9

IE6は、上の4つのバイト表現をすべて「©」(U+00A9)と解釈してしまいます。

このようにIE6のデコーダはかなりルーズにできています。それもあって、直後の1バイトが食いつぶされるだけでなく、先の例のように3バイト(もしくはそれ以上)が食いつぶされるような現象が発生します。

その他の方法

UTF-8以外ではどうかというと、(IE6・IE7では)EUC-JPの場合にXSSを成功させることができます。

しかも、UTF-8では「foo」「bar」の2つの変数を制御できなければ攻撃は成功しませんが、EUC-JPでは1つの変数に任意のバイトを入れられるだけで攻撃可能です。出力される箇所がSCRIPTタグの中でなくてもかまいません。

詳細はあえて割愛しますが、EUC-JPのデコーダもかなりおかしなことになっています。IE8ではかなり改善されていますが、それでもまだ中途半端なところがあります。