2020/05/18

CVE-2020-11022/CVE-2020-11023: jQuery 3.5.0で修正されたSecurity Fixの詳細

English version is here: https://mksben.l0.cm/2020/05/jquery3.5.0-xss.html


先月、jQuery 3.5.0がリリースされました。
このバージョンでは、僕が報告した問題がSecurity Fixとして含まれています。

jQuery 3.5.0 Released! | Official jQuery Blog
https://blog.jquery.com/2020/04/10/jquery-3-5-0-released/

報告したバグは、CVE-2020-11022、 CVE-2020-11023 として採番されています。

https://github.com/advisories/GHSA-gxr4-xjj5-5px2
https://github.com/advisories/GHSA-jpcq-cgw6-v4j6

少し遅くなりましたが、この記事では、この問題がどんなものであったかを紹介します。

問題の概要


この問題の影響を受けるのは、次のようなアプリケーションです。

  • ユーザに、XSSが起きない範囲で、好きなHTMLを使用させる機能がある
  • そのHTMLをjQueryで動的にページへ追加している

このような機能を持つアプリケーションを、シンプルなコードで表してみます。
<div id="div"></div>
<script>
//サーバ側でサニタイズされた安全なHTML
sanitizedHTML = '<p title="foo">bar</p>';
//divにHTMLを追加
$('#div').html(sanitizedHTML);
</script>
この状況では、通常、適切にサニタイズが行われているのであれば、ただ安全なHTMLを追加しているだけなので、XSSが発生することはないように思えます。しかし、実際には、.html()は内部で特別な文字列処理を行っており、この処理のせいでXSSが発生する場合がありました。これが今回の問題です。

XSSが起きる例


いくつかバリエーションがあるのですが、3つ例を示します。
次の3つはいずれも本来スクリプトが実行されないHTMLです。
例1.
<style><style /><img src=x onerror=alert(1)> 
例2. (jQuery 3.x以降のみ影響)
<img alt="<x" title="/><img src=x onerror=alert(1)>">
例3.
<option><style></option></select><img src=x onerror=alert(1)></style>
onerrorを持ったimgタグがあるように見えるかもしれませんが、よく見ると、属性内にあったり、style要素の内側に置かれていて、それらは実際には実行されないものです。これらがサニタイズ済みの安全なHTMLとして生成されたとしても、なんら不自然ではありません。

しかしながら、いずれも、.html()を通して追加されると、本来は実行されないスクリプトが実行されてしまいます。

それぞれの例を以下で実際に実行できます。
https://vulnerabledoma.in/jquery_htmlPrefilter_xss.html

なぜ実行されたのか詳しくみていきます。

CVE-2020-11023: 問題の原因(例1と2)


例の1と2は同じ原因で発生します。.html()の内部では、引数のHTML文字列が $.htmlPrefilter() というメソッドに渡されます。htmlPrefilterでは、<tagname />のようなself-closingタグを、次の正規表現を使った置換によって、<tagname ></tagname>のような形に戻す処理を行います。

rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi
[...]
htmlPrefilter: function( html ) {
  return html.replace( rxhtmlTag, "<$1></$2>" );
この置換処理に例1のHTMLを通した時の出力は以下になります。
> $.htmlPrefilter('<style><style /><img src=x onerror=alert(1)>')
< "<style><style ></style><img src=x onerror=alert(1)>"
黄色部分が置換された箇所です。この置換により、style要素の内側にあった<style /><style ></style>となり、それ以降の文字がstyle要素の外にはみ出てしまいました。.html()はこの後、置換後の文字列をinnerHTMLへ代入します。そこで、本来style要素の内側にあった<img ...>がタグとして現れ、onerrorが発火してしまいます。これが例1が発火するメカニズムです。

なお、上記の正規表現はjQuery3.x以前に使われていたもので、3.x以降は以下のように少し変更されています。

https://github.com/jquery/jquery/commit/fb9472c7fbf9979f48ef49aff76903ac130d0959#diff-169760a97de5c86a886842060321d2c8L30-R30
rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi

この変更によって、より簡単な要素・属性で、XSSを引き起こすことが可能になりました。
例2はこの変更によって生まれた新たなベクターです。jQuery3.x以降でのみ動作します。
> $.htmlPrefilter('<img alt="<x" title="/><img src=x onerror=alert(1)>">')
< "<img alt="<x" title="></x"><img src=x onerror=alert(1)>">"
こちらは属性内にある文字列がはみ出してXSSが発生しています。

これが例1と2の原因でした。

jQuery側の修正(例1と2)


$.htmlPrefilter()を、渡された文字列をそのまま返す関数に変更することで対処されました。

https://github.com/jquery/jquery/commit/90fed4b453a5becdb7f173d9e3c1492390a1441f#diff-169760a97de5c86a886842060321d2c8L201-R198

しかし、これで全ての問題が解決したわけではありませんでした。.html()内部ではさらに別の文字列処理が行われており、それが原因で例3が発生します。

CVE-2020-11022: 問題の原因(例3)


.html()内部では、引数に渡されたHTMLの先頭に出てくるタグが特定のタグであった場合、他のタグで一度ラップしてから処理を行おうとします。これは、ラップされている状態でないと、処理途中で勝手に消されてしまうような要素がブラウザの仕様やバグのために存在するからです。

option要素はそのような要素の1つです。IE9限定ではありますが、IE9のバグによってselect要素がないと消されてしまいます。jQueryはこれに対処するために、例えば、<option>aaa</option>のような、最初に出てくる要素がoption要素のHTML文字列が渡されると、<select multiple='multiple'></select>で全体をラップして処理を行おうとします。

ラップを行うタグは以下のファイルで定義されています。
https://github.com/jquery/jquery/blob/3.4.1/src/manipulation/wrapMap.js#L9

実際のラップは以下で行われます。
https://github.com/jquery/jquery/blob/d0ce00cdfa680f1f0c38460bc51ea14079ae8b07/src/manipulation/buildFragment.js#L39

例3は、このラップの処理が原因で発生していました。
例3のHTMLがラップ処理を通ると、次のHTMLが組み立てられます。
<select multiple='multiple'><option><style></option></select><img src=x onerror=alert(1)></style></select>
これがjQueryの内部のコード中でinnerHTMLへ代入されるとき、スクリプトが実行されます。

スクリプトが実行される理由はselect要素のパース方法にあります。select要素の内側では、option、optgroup、script、template要素以外のHTMLタグは置くことができません。この仕様によって、ここに書かれた<style>は無視され、<style>の内側にあった</select>が閉じタグとして作用し、select要素がそこで終了します。後続の<img ...><style>からはみ出た結果、onerrorが発火します。これが例3の原因でした。

jQuery側の修正(例3)


このラップ処理をIE9だけに適用することで対処されました。

https://github.com/jquery/jquery/commit/966a70909019aa09632c87c0002c522fa4a1e30e#diff-51ec14165275b403bb33f28ce761cdedR25

そうすると、IE9は脆弱なままに思えますが、実際には問題ありません。というのも、詳しくは触れませんが、IE9は他のブラウザとは異なる奇妙な規則でselect要素の内側に置かれた<style>のパースを行うため、この問題の影響を受けないからです。

ちなみに、これらの問題は、.html()だけではなく、.append()$('<tag>')のような、HTMLを生成/追加する際に$.htmlPrefilter()やラップ処理を通すその他のAPIでも発生します。

更新しましょう


サニタイズしたHTMLをjQueryで追加している覚えのある人は3.5.0以降へのアップデートを推奨します。
アップデートが何らかの理由で難しい場合は、DOMPurifyを使ってサニタイズを行うことをお勧めします。DOMPurifyは、SAFE_FOR_JQUERYという、このバグを考慮したサニタイズを行うオプションがあるためです。例えば、次のように使います。
<div id="div"></div>
<script>
unsafeHtml = '<img alt="<x" title="/><img src=x onerror=alert(1)>">';
var sanitizedHtml = DOMPurify.sanitize( unsafeHtml, { SAFE_FOR_JQUERY: true } );
$('#div').html( sanitizedHtml );
</script>
なお、DOMPurifyでは最近バイパスが見つかっています。既にDOMPurifyをSAFE_FOR_JQUERYと共に使っている人は、バイパスに対処した2.0.8以降にアップデートしていることを確認してください。

おわりに


この問題を調査したきっかけは、@PwnFunctionさんによる以下のXSSチャレンジでした。
https://xss.pwnfunction.com/challenges/ww3/

実はこのバグの一部は以前から既知で、このチャレンジではそれが想定解となっていました。(実際、DOMPurifyでは、2014年には既にSAFE_FOR_JQUERYオプションが導入されており、相当前から知られていた問題であることがわかります。)

今回、このチャレンジをきっかけに改めてjQueryのソースを読んでみました。その過程で、今まで言及されていなかった「例2」のベクターを発見しました。このケースは、かなりベーシックなHTML要素・属性だけで攻撃が可能で、多くの開発者が知らずに脆弱性を作りこんでいるのではないかと考え、実際に調べてみると、すぐにXSSが可能なアプリが見つかりました。影響を受けるアプリの開発元に報告をすると同時に、本来はjQueryが修正すべき問題だという思いを持ち、今回報告に至りました。jQueryのメンテナの方々は、破壊的な変更をしなければならなかったにもかかわらず、迅速に問題に対処してくれました。素早い対応に感謝します。また、この問題を調査するきっかけをくれたXSSチャレンジ作成者の@PwnFunctionさんにも感謝します。

以上、jQuery 3.5.0で修正された脆弱性について説明しました。この記事がjQueryを使ったアプリをセキュアにする助けとなれば嬉しいです。

0 件のコメント:

コメントを投稿