jQuery Migrate Plugin なんてなかった件

いくつかのサイトで実際に遭遇してますが,jQueyr 1.6.3 で解決( jQuery Bug #9521 )したはずの $("a[href=" + hash + "]") タイプの XSS が ( 2013/11/18 現在 1.2.1 を含む全ての) jQuery Migrate Plugin で復活します.

色々追ってみて,やっぱりまだ動くけどと言ったら,8月にすでに,実際バグだけどこれで想定通りなんだと言われてた(He told me that this was not, in fact, a bug, but was working as intended.) のでそういうことだったみたいです.

jQuery 本体側の変遷

jQuery$() には複数の機能があり,引数の型や数によって動作が変わります.$(function(){}) で呼ばれると,DOM Ready へのイベントハンドラになり,$(String) で呼び出されると,CSS 風のセレクタとして要素の選択になったりHTMLタグっぽいと要素生成になったりします.

この $(String) でのセレクタと要素生成を判別しているのが,quickExpr という変数で,以下の様な変更がされてきました.

  • jQuery 1.6.2 quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,
  • jQuery 1.6.3 quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,
  • jQuery 1.9.0 rquickExpr = /^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/,
  • jQuery 1.10.0 rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,

1.6.2 までは,# から始まる文字列を id セレクタ扱いにしているように見えて,前半部分は (<以外なんでも)<(だいたいなんでも)>(>以外なんでも) という文字列と,後半は # と英数字のみと一致するため,# などだいたいが前半の要素生成文字列とみなされ,id セレクタとみなされる条件がかなり弱くなっていました.

これを 1.6.3では jQuery Bug #9521 で,前半を (<と#以外なんでも)<(だいたいなんでも)>(>以外なんでも) に変えたため,# が含まれていたら id セレクタ(を含むセレクタ)とみなされて Sizzle 側でエラーとなるようになりました.

$(location.hash)$("a[href=" + location.hash + "]") には効果がありましたが,$("." + location.hash.substring(1)) というような,# を含まない文字列での要素生成の問題は残り,1.9.0 jQuery Bug #11290 でひとまず解消しました.

1.9.0 で要素生成は先頭が < の時に限られるようになりました.$(location.hash.substring(1)) などとしていれば当然要素生成になってしまいますが,CSS セレクタ として < が1文字目に出てくることはないので,実質的にこれでセレクタのつもりが不意な要素生成という問題は起こらなくなったはずでした. その後,1.10.0 では先頭の空白が許容されるようになりましたが,これは,クライアントサイドでのテンプレートが壊れる問題 への対応策として導入されたもので,セレクタとしての振る舞いに影響を与えないので特に問題はありません.

jQuery Migrate Plugin の変遷

1.9.0/2.0.0beta と同時に Migrate Plugin がリリースになりました.Migrate Plugin では$(String) の振る舞いもPlugin の対象領域になりました.

セレクタと要素生成の判別処理に関しては当初,1.6.2 と同程度にまで退行しました.

jQuery.com が退行した Migrate Plugin 1.1.0 と jQuery 1.9.0 とで構成されていたため,jQuery Migrate is a Sink, too?! で潜在的な脅威として取り上げられ,XSS · Issue #36 · jquery/jquery-migrate となり,1.2.0 に反映となりました.

その後,属性値内の # が XSS と誤認識される問題改行コードを入れられない問題先頭に < をという警告が出る問題 があり,1.2.1 が出ましたが,1.6.2 相当でも 1.6.3 相当でもない中途半端な現在に至ってます.

一応 jQuery Migrate Plugin 1.0.0 の頃から rquickExpr の上にコメントで // Note this does NOT include the # XSS fix from 1.7! と書いてあるので,互換性は維持されるが脆弱になることは考慮していたと思われますが,jQuery Migrate Plugin 1.2.1 でこれが削除され,// Note: XSS check is done below after string is trimmed に書き換わりましたが本質的には何も変わってません

Flash LSO XSS のお話

3行で頼む

swfobject.js 附属の ExpressInstall.swf を 適当な引数で呼び出して すったもんだの末に www.macromedia.com で alert でたよ.

はじめに

swfobject.js がアレな話 の延長戦的な方向で ExpressInstall に絡んでこういう事例がありましたというお話です.

アレな話の時には,主として 「swfobject.js を使って swf を設置しているページで,不適切な使い方をしてしまうとそのドメインで問題が起きます」という話でしたが,今回は,「不適切な expressInstall.swf ( autoUpdator.swf ) の呼び出しによって flash Plugin のインストール後に www.macromedia.com 上で任意のスクリプトが実行される」というお話です.

これまでの話

ExpressInstall についてはアレな話にも書いてあるわけですが,復習を兼ねてもう少し詳しく,その流れを書くと以下のようになります.

  1. 表示させたいページが swfobject.js で最低限必要な Flash Player Plugin のバージョンを指定する
  2. 指定されたバージョンより新しいのがインストール済みなら目的の swf を貼り付けてお終い.
  3. 指定されたバージョンより古いのしか無かったら目的の swf の代わりに ExpressInstall.swf を呼び出すコードを貼り付ける.このとき,どこの URL から呼び出されたのかが MMredirectURL という変数(flashVars)を介して ExpressInstall.swf に渡される
  4. ExpressInstall.swf は ftpdownload.macromedia.com にある autoUpdater.swf を読み込む
  5. autoUpdater.swf がプラットフォームごとに最適なインストーラをダウンロードして起動
  6. インストールウィザードなりでインストール.
  7. インストーラの最後で覚えておいた URL にリダイレクト.
  8. Player が必要なバージョン以上になっている(はずな)ので目的の swf が表示されるはず

アレな話のときは,3番に問題があったので表示させたいページで XSS となっていましたが,今回はの問題の舞台は7番にあります.

今回の話

簡単に「インストーラの最後で覚えておいた URL にリダイレクト.」と書きましたが,これはだいたい以下の様な流れで実現されています.

  1. 任意のドメイン上で ExpressInstall.swf (以下,ei.swf)が MMredirectURL として flashVars から戻り先 URL を受け取る.
  2. ei.swf が http://fpdownload.macromedia.com/pub/flashplayer/update/current/swf/autoUpdater.swf (以下,au.swf)を読み込む
  3. au.swf に redirectURL として ei.swf から渡す.
  4. au.swf が渡された redirectURL を macromedia.com ドメインの LSO (Local Shared Object) に redirectSO.data.url として保存
  5. (au.swf からインストーラをダウンロード.)
  6. インストール終了後,ブラウザでリダイレクトページ http://www.macromedia.com/software/flash/about/installerRedirect.html を開く
  7. リダイレクトページで http://www.macromedia.com/software/flash/about/so_redirect.swf (以下,so.swf)が読み込まれる
  8. so.swf は au.swf が LSO に保存した redirectSO.data.url を読み出す
  9. so.swf は fscommand を使って LSO から読みだした URL を引数にリダイレクトページのスクリプトの SOredirect_DoFsCommand をコール.
  10. SOredirect_DoFsCommand が window.location.reload( url ) して戻り先 URL にリダイレクト

構成要素はいろいろ有りますが,戻り先 URL の流れに着目して単純にすると

  1. 任意ドメインでの ei.swf
  2. macromedia.com の au.swf
  3. macromedia.com の LSO
  4. → www.macromedia.com の so.swf
  5. → www.macromedia.com のリダイレクトページの window.location

という流れで URL が渡っていきます.

ところが,どこにも URL を検証する処理がないので,こんなかんじで ei.swf を呼び出すときに flashVars を細工してしまうと最終的に www.macromedia.com 上で任意のスクリプトが実行されるということになります.

gist6511895

よくわかりません

映像にするとこういうことです.


20130418002229 - YouTube

影響とか

よくわかりません.

どこか怪しげなサイトで言われるがままに Flash Player のインストールをしてた場合,www.macromedia.com 上で任意のスクリプトが実行されてぬとねの区別がつかない事になってたかもしれません.

あるいは macromedia.com 上のリソースのほとんどが adobe.com へのリダイレクトになっていることと,一部のブラウザが XHR 時のクロスドメインのリダイレクトに対して同一ドメインへの XHR として扱ってたことから,adobe.com 上のリソースにアクセスされてたかもしれませn.

まとめ

flash の LSO も使いようによっては XSS に使えるので開発者の方々は御留意頂ければ幸いです.

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

クロスブラウザな css3 linear-gradient が面倒になりそうな件について

2012 年の 4月に CR(勧告候補) になった,CSS Image Values and Replaced Content Module Level 3 とそこで規定されてる,Linear Gradients: the ‘linear-gradient()’ notation の幾つかのブラウザでの対応具合が面倒な事になってるなぁってお話.

経緯とか

最初期

もともと,グラデーションだーと提案された頃には,WebKit の独自実装としてこんな感じの書式だった.(cf. Introducing CSS Gradients)

background: -webkit-gradient(linear, 
  left top /* 始点 */,
  left bottom /* 終点 */, 
  from(#000) /* 始点の色 */, 
  to(#FFF) /* 終点の色 */);

途中にもっと色を挟むとか透明度を変化させるとか細かい指定は割愛.

これに対して Gecko(Mozilla) が提案してきたのが,以下の様なちょっと違う書式だった.

background: -moz-linear-gradient(
  top /* 始点 */,
  #000 /* 始点の色 */, 
  #FFF /* 終点の色 */);

このあたりはMDNの linear-gradient #History of_the syntax を参照.

混迷期

当然といえば当然だが,当初から別の書式が提案されてきたために,2010年頃には2つの「独自実装に対応する」にはこう書くといった記事や,Gradient generator などと呼ばれる各実装用のコードをお手軽に生成するようなサービスが跳梁跋扈する運びとなっていた.

ここで,さらに IE 用に filter を使ったり,Opera 用に SVG を使ったりと言った,そもそも working draft であるとか,vendor-prefix 付きであるとかいうことの意味を無視したかのような how to や主張が見られたり,揺り返し的に vendor-prefix 再考といった動きにつながったりしてたけど,そのへんはまた別のお話.

background: #000000; /* Old browsers */
/* IE9 SVG, needs conditional override of 'filter' to 'none' */
background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwMDAwMCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmZmZmZmYiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+);
background: -moz-linear-gradient(top,  #000000 0%, #ffffff 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#000000), color-stop(100%,#ffffff)); /* Chrome,Safari4+ */
background: linear-gradient(top, #000000 0%, #ffffff 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#000000', endColorstr='#ffffff',GradientType=0 ); /* IE6-8 */

Ultimate CSS Gradient Generatorでの生成から調整.

さらにこれに CSS 外(IE のコンディショナルコメントや navigator.userAgent などのブラウザ判定など)といろいろな組み合わせて何とかしてた.

安定期

で,2011年に入って,まず,WebKit -moz-linear-gradient と同じ書式に対応,4月にはOpera が 11.10(Presto/2.8.119) で -o-linear-gradient() として対応,5月にはIE10pp1 で -ms-linear-gradietn() として対応 となって,vendor-prefix 付ける必要はあるけど,ほぼ同じ書式に統一されて,linear-gradient が使えるようになった.

この頃になると,古いブラウザはとりあえず置いといて広く使えるようになってきたことや,open web やら,CSS フレームワークの普及やらで,以下の様な書き方がされるようになり一応の収束を見る.

 background: -webkit-linear-gradient(top, #000, #fff);
 background: -moz-linear-gradient(top, #000, #fff);
 background:   -o-linear-gradient(top, #000, #fff);
 background:  -ms-linear-gradient(top, #000, #fff); /* for ie10 platform preview */
 background:      linear-gradient(top, #000, #fff);

ところが,ここで,大元の CSS3 の仕様に破壊的な変更が入り再び混乱期に突入となる.

再混乱期?

20110712版のWDでの変更が始まりである.

linear-gradient では方向の指定に top/bottom/left/right といったキーワードの他に,角度(deg) での指定ができる.

20110712 版までは,上が黒で下が白のグラデーションの場合,(top, #000, #fff) と書くか,(-90deg,#000,#fff) と書いていた.角度の数値は,X軸正の方向を0,Y軸正の方向を90とし,反時計回りに増加する数学的な象限と同様だった.

これが,20110712 版以降では,Y軸正の方向を0,X軸正の方向を90とし,時計回りに増加する座標系が採用され,これまでと同様の効果を得るには,(180deg,#000,#fff) と書くように変更された.

これは他の CSS の仕様や SVG の仕様との統一性の観点から議論となり変更となった(参照 CSSグラデーションでの角度の解釈 ).

20120712 版での変更を踏まえて,次の20110908版のWDで,キーワード to が追加され,これまでの topto bottomへ,leftto right へと変更になった(参照 CSSグラデーションの構文変更とベンダー接頭辞).

この仕様変更に追随したのが,MozillaOpera だった.

まず,Mozilla685400 - Add a new syntax to -moz-linear-gradient per latest css spec にて,to 有りと無し両方の記述に対応することになった.

Opera は 11.60 あたりからやはり,有り無し両方の記述に対応することになった.(CSS3 Image Values/Replaced Content support in Opera Presto 2.9#linear-gradient(); 11.5x が Presto 2.9.x なので対応しているように読めるが Desktop 版 11.52 では to 無しのみ対応 )

ただし,MozillaOpera も vedor-prefix がついているうちは,20110712 版での角度での変更には対応せず,従来通りの反時計回りを正とする指定のままとなっている.

つまり,古いブラウザをある程度捨てたとしても,仕様と各ブラウザの実装との折り合いをつけると以下の様に書くことになる.

 background: -webkit-linear-gradient(top, #000, #fff);
 background:    -moz-linear-gradient(top, #000, #fff);       /* -moz-liner-gradient(to bottom, #000, #fff); */
 background:      -o-linear-gradient(top, #000, #fff);       /*   -o-liner-gradient(to bottom, #000, #fff); */
 background:     -ms-linear-gradient(top, #000, #fff);       /* for ie10 platform preview */
 background:         linear-gradient(to bottom, #000, #fff);

角度での指定では以下の様になる.

 background: -webkit-linear-gradient(-90deg, #000, #fff);
 background:    -moz-linear-gradient(-90deg, #000, #fff);    /* -moz-liner-gradient(180deg, #000, #fff); */
 background:      -o-linear-gradient(-90deg, #000, #fff);    /*   -o-liner-gradient(180deg, #000, #fff); */
 background:     -ms-linear-gradient(-90deg, #000, #fff);    /* for ie10 platform preview */
 background:         linear-gradient(180deg, #000, #fff);

ここまでが,2012 年8月現在の状況である.

2012 年の秋冬に待ち受ける事態

さて,問題はこれからの話しにある.再混乱期で十分に面倒な雰囲気にはなりつつあるが,2012年の秋か冬には少なくとも 2つ変化がやってくる.

IE10 のリリースと Opera 12.50 のリリースである.

IE10 での linear-gradient

Windows 8 Release Preview に搭載された IE10 では,toキーワードありのみに対応し,角度の指定は CR である 20120412版に対応する.なおかつ -ms- の vendor-prefix を持たない.Unprefixed CSS3 Gradients in IE10.

したがって,-ms-linear-gradient の付いたものを単純に削除して,IE10 用は(CR以降の)新しい書式を使うことになる.IE9 以下用はこれまで通り,コンディショナルコメントの向こう側に押し込めるなどしておく.

 background: -webkit-linear-gradient(top, #000, #fff);
 background:    -moz-linear-gradient(top, #000, #fff);       /* -moz-liner-gradient(to bottom, #000, #fff); */
 background:      -o-linear-gradient(top, #000, #fff);       /*   -o-liner-gradient(to bottom, #000, #fff); */
 background:         linear-gradient(to bottom, #000, #fff);

Opera 12.50 での linear-gradient

Opera 12.50 では,2012/08/10 の Snapshot/ 12.50.1546 から vendor-prefix が無くなり,IE10同様,to 有りのみに対応し,角度の指定も CR 版に対応するようになった.さらに -o- 付きの指定は,すべて無視されるようになった.

したがって,vendor-prefix 付きで古い書式と vendor-prefix 無しでの CR 版以降の書式を併記することで,Opera 11.1x 以降には特に問題なく適用されることになる.

今後やってくる面倒な事態の可能性

つまるところ,混乱期や安定期の頃によく見られた vendor-prefix 付きと無しと同じ内容でつらつらと列挙した CSS では,IE10 や Opera 12.50 以降に対応できない.top や left の指定では完全に無視されて背景がでないといった事態になり,角度での指定では全く違う方向のグラデーションが指定される事態になる.

再混乱期?の節で理想的な指定方法を挙げたが,実際にこの様に to や角度の意味論を vendor-prefix の有り無しで分けて書いている例をそれほど見ない(前述の,Ultimete Gradient Generator は対応済み).CR 版やその前段階の 20110711 版での仕様変更に関する言及も,当初 gradient が喧伝された頃に比べ少なく,それらの古い仕様での解説記事が更新されない可能性も高い.さら,すでに稼働中のコードはいろいろな事情で更新されない可能性が高いため,「OS を新しくしただけ」,「ブラウザを新しくしただけ」で,サイトが使い物にならなくなったとなる事態が少なからず発生する可能性が高い.

まとめ

Linear Gradients: the ‘linear-gradient()’ notation のこれまでの経緯と,2011年秋にあった全く意味が異なるほどの仕様変更と,2012年の秋冬にはこの仕様にのみ対応するブラウザが少なくとも 2つリリースされる予定なので,関係者の方はそろそろ新しい仕様への書き換えが必要なんじゃないでしょうかというお話でした.

追記

 

続編 (どう書くのがベターかって話,Firefox 16 での話,Opera 12.50 が 12.10 になって -o- 付きが復活した話) あとで書く

Opera 12.50 の場合,さらに -webkit-prefix に絡んだ話があり,すこし面倒くさい

Mobile の方で -webkit- ばっかりだから,うちも -webkit-対応するぞ,と何言ってんだこいつらみたいな発表があって,その後実際に,Desktop 12.50.1497 で実装.この時点でいくつかの CSS プロパティは -o- と -webkit- 両対応となった.さらに,12.50.1546 で,このうちのいくつかについて -o- が外れることになり,プレフィックスなしと -webkit- 付きとがサポートされることとなった.

12.50 以降の liner-gradient() はこの,プレフィックス無しと -webkit- 付きとがサポートされるプロパティのうちの一つなのだが,-webkit- 付きの方は 20110712 版以前の古い書式のみがサポートされ,プレフィックス無しの方は CR 版のみがサポートされている.(cf. CSS vendor prefixes in Opera 12.50 snapshots)

このため,新旧の書式と,-o- を含めたプレフィックスのある無しとを列挙するときには,その順番等によって異なる結果を生む.

基本的に,CSS の一般的な文法に則って,「最後に指定された有効なプロパティが適用」される.

http://jsfiddle.net/4wHLD/ のサンプル群は,Opera 11.1x+ ではすべて同じ白から緑へのグラデーションになるが,12.50 では,いくつかのケースで異なる結果となる.

例えば,2a のケースでは,プレフィックス無しだ採用されそうだが,to キーワードも無いため,結局 -webkit- 付きが採用され,白から青へのグラデーションと成っている.2b のケースでは,プレフィックス無しの指定が採用され白と赤のグラデーションになっているが,角度の解釈が新仕様のため方向が上下ではなく左右となっている.

 

Disqus にいくつか DOM based XSS があった件

概略

サイトのコメント欄の外部委託系サービスの disqus.comにいくつか DOM based XSS がありまして修正されました.location.hash 部分の url encode が行われない IE, Chrome, Opera で問題があったことは確認しています.

まず一つ目は,コメント欄の埋め込みスクリプトにありました.

脆弱性とその修正を確認しているのは,blogid.disqus.com/admin/universal/ にて表示されるコードを埋め込むタイプで,blogid.disqus.com/embed.js というスクリプトです.

こちらのコードは,埋め込んだドメイン上で動作していましたので,このタイプのコードを埋め込んでいたドメインで提供されているサービスに影響があった可能性があります.disqus.com ではuniversal code とは別に,特定の幾つかのブログサービス向けに専用の手順やコードを用意していますが,そちらで利用していた場合にも,影響があった可能性があります.

もう一つは,disqus.com 本体の,ログイン周りのコードです.disqus.com の幾つものページでログインしていない状態でのみ使われるコードに問題がありました.

2012/08/24 17時現在はどちらも対応済みです.

時系列

  1. 最初に気がついたのが,08/21 の 12時前

  2. 実際のコードと再現手順を確認するためにアカウントを新規で作ったあと,コードを検証してコンタクトフォームから通報して,定型の応答が来たのが  08/21 の 12:40 頃.

  3. その後しばらく,音沙汰が無いまま 45時間後の 08/23 の 9時過ぎに「担当チームに回したよ」とのメールが到着.

  4. メールに気がついた,11時半頃に一応の修正を確認.

  5. 前後して,disqus の embed の status が一時的に不調

  6. 色々確認して,13時半過ぎに直ったと Tweet.

  7. 19時過ぎにそれどっち側の話?とやりとり

  8. 08/24 14 時ごろに disqus.com 本体側のコードも修正.

  9. 08/24 17 時ごろにひとまず記事に

技術的な詳細

埋め込みコメント欄

disqus の埋め込みコメント欄は,埋め込まれる側の html に div#disqus_thread とスクリプトを追加してもらい,スクリプトでこの div の中にコメント欄を形成する形になってます.

コメント欄本体は disqus.com の iframe 上に作られてますが,うまく iframe が読み込めないなどに備えて,div の直下にいくつか要素が追加されます.

この追加されるコードで location.href をそのまま innerHTML に挿入している以下の様なコードがあり,div の下に iframe のコメント欄が形成される一瞬の隙をついて location.hash に載せたコードが実行可能な状態になってました.

q = f.setTimeout(function () {
 t.innerHTML += '<p>Loading DISQUS seems to be taking longer than usual, <a href="' + f.location.href + '">reload</a>?</p>'
 },1000)

記事執筆時点の実装では,onclick で location.reload() するように変更されました.

disqus.com 本体

disqus.com では,ログイン・ログアウト用のパーツの生成処理にテンプレートを使ってます.

このうちログイン用のパーツで,ログインしたあとの遷移先の指定が以下の様なテンプレートになっていました.

<li data-analytics="header login"><a rel="login" href="<%= data.urlMap.login %>?next=<%= document.location.href %>">Login</a></li>

このため,テンプレートが展開されてページの DOM に追加されるときに,location.hash に載せたコードが実行可能な状態になってました.

これが現在は,encodeURIComponent を使う形に修正されました.

本当のログインフォームと向き合え(て)ますか

 

どのページからでも,ログインフォームがオーバーレイで表示されてログイン出来る UI は本当に必要なのかという疑問.

 

専用ページの場合

  • そのページ(とフォームの送信先)を SSL にするだけですむ
  • XSS の心配も限られた要素だけですむ
  • そもそも JS 無しに出来てDOMXSS を気にしなくてすむ
  • JS無し,CSS 無しの人でも使える

どこでもログインの場合

  • フォームの送信先はもちろんのこと,ログインフォームを置くすべてのページで SSL 化すると面倒
  • ページ表示用のあらゆる要素もXSS の心配する必要
  • JS 使いまくりの場合DOMXSSの心配も必要
  • JS 無し,CSS 無しの人でも使えるようにするのが面倒

とデメリットのほうが先行してしまって,ログインのためのページ遷移の削減がそれほどメリットにならないなと.

 

ところが,ユーザ側の視点からすると,

  • そもそも別ページだろうとその場でだろうとフォームの送信先がどこかなんていちいちきにしてない
  • さらに SSL かどうかたいして気にしてない
  • JSも CSS も切ってないから何の問題もない
  • ログインしたらすぐ使いたいからページ遷移で待たされたくない

という感じでどこでもログインでなにも問題がない.

 

というわけで,ユーザ側からするとどこでもログインできる方がいいけど,サービス提供側としては特にメリットがないよなぁという話で,特に落ちはない.

XHR XSS の話.

概要

CORS が「幾つかのブラウザの先行実装」の状況から「古いブラウザではサポートされない機能」に変わりつつある頃合いなので,XHR2 が XSS の起点になりますよってお話.

そもそも XHR XSS って何よ

簡単に言うとXHR2 による XSS のことのつもり.身近なところだと,jQuery Mobile がやらかしたり大阪府警がやらかしたりした.

具体例1 jQuery Mobile

jQuery Mobile については,jQuery MobileのXSSについての解説 で解説されるとおり.

かいつまんで言うと,jQuery Mobile に location.hash の変更( hashchange イベント発火)時に,location.hash を URL とみなして読込んで,ページ内容を変更という機能があって,その読込先 URL にクロスドメインの制約がなかったので XHR2 可能な環境では,location.hash に XHR2 で読込み可能な URL を指定しておくとそのページを読み込んだだけで,悪意あるコンテンツに汚染されるというモノ.

beta2 ではクロスドメインでの読込みに明示的な許可フラグの設定が必要になっているのでひとまずは収束している.

具体例2 大阪府警の話

大阪府警のサイトでは,Prototype.jsAjax を用いて jQuery Mobile の例の様なページ遷移のない内容の切り替えが実装されている.(2012/03 現在)

この内容の切り替え先のURL に一応の制約(fromHash.match(/^[a-z_\-\.\/]+$/i))があったが,'//evil.example.com/foobar' のように '//' からはじめることで切り替え先を任意の URL にされる問題があった.

PoC が公開されてすぐに,切り替え先が '//' を含まず,かつホワイトリストに完全一致した場合にだけ遷移するように修正され収束した.

CORS 時代の XHR とのお付き合い

元々ドメインを超えたリソースへのアクセスの需要には,メジャーなブラウザでは IE が 10pp4 ( 2011年11月末 ) まで,Opera が 12.00 (Presto/2.10.232) まで対応してなく,JSONP,iframe による代替手法の供給や IE8 での専用 APIXDocumentRequest の存在により,クロスドメインのリクエストを意識的に別物として扱って来た経緯がある.

そのことが XHR.open() のリクエスト先 URL に対して,jQuery Mobile のように,そもそも意識されてなかったり,府警の例の様に '//' から始まる URL がドメインを超えるアクセスになることが見落とされたりといった状況を産んできたように思う.

じゃあどうするかというと,幾つかの状況に応じて対応していく必要がある.

外部のAPIへのリクエストを出す場合

XHR2 で外部の API のリソースを取得する場合は,想定した API ではない URL へのリクエストになってないかに気をつける.

例えば, 以下の様なコードでディレクトリトラバーサルを検出するなどやり方は色いろあると思う.

var apiurl = 'http://api.example.org/some/api/point/' + cook(params); 
var absurl = (function(path){ var a = document.createElement('a'); a.href = path; return a.href;})(apiurl); 
if ( apiurl != absurl ) { alert('something wrong'); }

同一オリジンへのリクエストを出す場合

同一オリジンへのリクエストしか行わないときでも,外部の API へのリクエストのように,リクエスト先ドメインを外部から変更できないような箇所に入れておいて http://... から始まる url を組み立てれば良い.

例えば,

function xhrget( path ){ 
  var xhr = new XMLHttpRequest(); 
  xhr.open( 'GET', path ); 
  /* more code... */ 
}

などなっていたのを

function xhrget( path ){ 
  var xhr = new XMLHttpRequest(), 
      absurl = 'http://self.example.com/' + path; 
      /* or absurl = [location.protocol, '//', location.hostname, location.port ? ':' : '', location.port, path ].join( '' ); */ 
  xhr.open( 'GET', absurl ); 
  /* more code... */ 
}

というように,リクエスト先ドメインをリテラルで指定するか location の hash や search 以外の箇所から組み立てるかすれば path が 'http:' や '//' から始まっていても意図しない外部リソースへのアクセスは発生しなくなる.

ユーザー入力の任意の URL に対して XHR したい場合

html を取得して利用する場合なら,XHR ResponseTypedocument にして取得することで script が取り除かれて動作してない状態の DOM を手に入れられるので,それで何とかする.ResponseType をサポートしてない場合や,それ以外の形式のリソース,例えば任意の JSON を取得してどうにかするとかは狂気の沙汰なので中継サーバを挟むといったブラウザの範囲外で対応するのがベターと思う.

まとめ

IE10 や Opera 12 の登場でモバイル端末がすでにそうであるように XHR がドメインを超えられるようになるので,リクエスト先には十分注意.

任意の文字列が突っ込めるようになってると当然にXSS となるし,絶対パスを渡すつもりで '/' から始まるようにしても '//' から始まるようにされるとこれも XSS になるので,外部から変更不能な形でドメインを設定してから XHR.open に渡すように.

任意の URL への XHR2 を安全に使うのは無理ゲーなのでやめる.

追記

JSON に何の問題があるのと指摘をうけたので整理と追記.

XHR XSS と書いたが,XSS となるのは以下の前提条件すべて満たす必要がある.

  1. ページの閲覧者が制御可能な箇所から XHR の完全なリクエスト先を決めていること
  2. XHR で取得したリソースを取得元のリソースの DOM かスクリプトの挙動に影響を与える方法で注入していること

jQuery Mobile の場合は,このどちらもをフレームワークが全自動で満たしていたためフレームワークを用いているすべてのページを細工された URL 経由で読み込むだけで,XSS になってた.大阪府警の例の場合は location.hash からリクエスト先を作っていたことで1つ目の条件が満たされ,取得したリソースを文字列として加工した後 HTML として元のページの DOM に注入していたことで2つ目の条件が満たされ XSS になってた.

CORS 時代の XHR とのお付き合いで書いたことのうち,「想定した API ではない URL へのリクエストにならないようにする」ことや,「リクエスト先ドメインを外部から変更できないようにする」ことは,XSS となる前提条件のうち1つ目の条件を満たせないようにするための措置.

実際には,想定した URL へのリクエストになるようにしても,前提条件の2つ目を満たさないようにはしていないので,リソースを利用する段階で API が変なモノを返してきてたり,同一ドメイン内の変なモノを拾ってしまったりで,スクリプトが実行される可能性は依然として残る.任意のURL から取得する場合よりずっと可能性は低いけど,安全に扱うための手段を講じておくほうがいい.

任意の URL に対する XHR で HTML を取得するときに ResponseType を指定するというのは,この記事での具体例1,2で示したような「別ページの内容(文字列)を HTML 断片として今のページの DOM に注入する」典型的な状況で,XSS となる前提条件の2つ目を満たせないようにするための措置.もし,XHR で取得した任意のリソースから生成した DOM を更に加工して何処かに利用した場合,XSS となることもある

JSON の場合も,任意の URL から取得してきて JSON.parse でオブジェクトにしただけでは(ブラウザにバグがあれば別だが),前提条件の2つ目を満たせてないので XSS とならない.取得元ページで加工し利用する際に,XSS になる可能性は HTML を取得してきた場合や,自ドメインからの取得の場合と同様に存在する.JSON.parse すればいいのではないか,クロスドメインで JSON を受け取っても問題ないと思う というのはその通りで,取得だけで XSS になるかのような「狂気の沙汰」と書いてたのは言いすぎだったと思う.