jQuery 1.9 の $.parseHTML とかその辺

まえがき

2013/01/15 に jQuery 1.9 と 2.0 ベータがリリースされて,サポートブラウザがどうとか互換性がどうとかいうお話がちらほら出る中,jQuery 1.6.3 から続く jQuery('セレクタだと思ったら要素生成でこんにちはこんにちは') 問題 への対応に一応の終止符が打たれたのでいろいろ書いてみる.

ver 1.6.2 以前

jQuery の 1.6.2 までは $(String) としたとき,「String になんか( HTML の)タグが入ってるっぽいぞ」と判断すると要素を生成し,そうじゃなければ CSS 的なセレクタとして振る舞うという機能がありました. 大抵の場合,大きな問題はなかったのですけども,ユーザ入力からセレクタを組み立てるときに問題になりました.

とくに '#' を含んだ文字列で ID セレクタとして振舞わせようとするのが典型的で,なかでも頻出していたのが,$(location.hash) というコードでした. このコードが,http://example.org/#hoge という URL で実行されると id="hoge" な要素の選択になるのに対して, http://example.org/#<img src="/" onload=".."> という URL のときは,img 要素の生成となります. このため onload や onerror を経由して,攻撃者は閲覧者の環境で任意のスクリプトを実行させること可能となっていました.

詳しくは,mala20110624 で解説されているとおりです.

ver 1.6.3 での対策

この 「ID セレクタとしての振る舞いを期待してるのに任意のスクリプトの実行が可能」という問題への対策として,まず 2011/09/01 に jQuery 1.6.3 がリリースされました. 1.6.3 では,「'#' を含まないタグっぽい文字列」なら要素生成,「先頭が '#'」 ならセレクタとして振る舞うようにして,``$(".foo #bar_" + location.hash.substring(1)) というようなコードで location.hash にタグっぽい文字列が入ってても要素生成とならなくなりました.

ID セレクタとしての挙動を期待するコード ( ex. $(location.hash) ) は本当に多くのコードやプラグインで使われていたため,1.6.3 で対策されましたが,それよりもやや稀な '#' を含まない class セレクタや属性値のセレクタを動的に生成するケースでの問題は依然として残されました.

$.parseHTML の登場

この問題の根本的な解決方法として採用されたのが「複雑になりすぎた $(String) の機能をある程度縮小 ー 要素生成との判断を厳しく ー して,明確に要素生成を望むのならば別のメソッドを呼ぶことにしよう」という方法で,その別のメソッドというのが $.parseHTML でした.( cf. jqbug11617 )

$.parseHTML では渡された文字列とオプションから jQuery オブジェクトではない素の DOM Node の配列を生成して返します.こうすることで,$(String) では要素生成と判定されなくなってしまうような文字列からも要素を生成できるので代替手段への切り替えを促しつつ $(String) の機能を変更する事ができるようになりました.

$.parseHTML は jQuery 1.8 beta1 で採用されましたが,$(String) の条件の方は,beta1 リリースの前に多少の混乱が有り, ver 1.6.3 の頃と変わらないまま '#' がアレばセレクタとして扱うままでした.

ver 1.9/2.0beta での対策

jquery 1.9 で $(String) の条件が変更され (cf. jqbug11290 ) ,文字列の先頭が '<' でその後に '>' があれば要素生成,'#' が先頭ならセレクタとなり,class セレクタや属性値のセレクタが要素生成になることはなくなりました. つまり,$(String) で要素を生成するには $("<div></div>") のようになっている場合だけで,$(".foo ." + location.hash.substring(1)) でも $("a[name="+location.hash.substring(1)+"]") でも要素の生成を心配する必要がなくなりました.

$.parseHTML は XSS 対策の銀の弾丸ではない

前述してきた通り $.parseHTML が生まれてきた背景には $(location.hash) による DOM based XSS の問題への対応という要素がありますし,jQuery のリリースでこの問題が扱われるときに「 XSS 対策として」というような文脈で扱われてきたため, メディア等での紹介記事も XSS 対策の一つとして触れていますが,$.parseHTML を扱った bug 11617 のコメント12 にもあるように,$.parseHTML は $(String) を簡略化した分の受け皿として作られてるので,このメソッド自体はより慎重に扱わねばならない場合もあります.

$.parseHTML Sample に書いたように,イベントハンドラは実行できるので単純に $(String) を $($.parseHTML(String)) に置き換えただけでは,事態は何も改善しませんしむしろ悪化することもあります.無害化というような効果はありません.

追記

Migrate Plugin を入れると全てチャラになります. jQuery Migrate Plugin なんてなかった件

bib