evalの第二引数とGreasemonkeyのunsafeWindowについて

http://d.hatena.ne.jp/brazil/20070420/1177060289」でFirefox専用の第二引数に環境の指定をとるevalの使い方について説明されています。その元はこちら「http://www.tom.sfc.keio.ac.jp/~sakai/d/?date=20070414#p02」で、僕もこれを見たときに初めてしりました。そのころ僕はグリモンのスクリプトをいじっていたので、ちょっと気になっていくつか試してみて、ネタ元のページに以下のようなブクマコメントをしました。

evalの第2引数しらなかった。これを応用すると、GreasemonkeyでusafeWindow.xxx() とする場合、xxx内部からeval("GM_xmlhttpRequest", arguments.callee.caller)でGM_xmlhttpRequestが取得されてしてしまう。usafeWindow経由で関数呼んじゃダメ。

http://b.hatena.ne.jp/entry/http://www.tom.sfc.keio.ac.jp/~sakai/d/?date=20070414%23p02

さらにそのメタブクマで、

さらにuser.js内部でunsafeWindow.jsonpcallback=function(){...} などとする場合、外部から eval("GM_xmlhttpRequest", jsonpcallback) でも GM_xmlhttpRequest が漏洩してしまう。これでドメインの壁が越えられて夢ひろがりんぐ…じゃなくて、超危険。

http://b.hatena.ne.jp/entry/http://b.hatena.ne.jp/sawat/20070416%23bookmark-4478097

で実際のところ、以下のuser.jsがGreasemonkeyにインストールおよび活性化されている場合、

// ==UserScript==
// @name           test
// @namespace      test
// @include        *
// ==/UserScript==
unsafeWindow.myobject = {};

攻撃者は以下のコードでGM_xmlhttpRequestを取得し、任意のURLへリクエストを発行できてしまいます。

gm_xhr = eval("GM_xmlhttpRequest", window.myobject);

あるいは、以下のようなuser.jsだった場合でも、

// ==UserScript==
// @name           test
// @namespace      test
// @include        *
// ==/UserScript==
with(unsafeWindow) {
  alert('Hello, World.');
}

以下のよう関数を上書きすることで、同様にGM_xmlhttpRequestオブジェクトを取得できます。

window.alert = function(str) {
  gm_xhr = eval("GM_xmlhttpRequest", arguments.callee.caller);
};

今まではGM_xmlhttpRequestのが漏洩に関しては「関数の引数にGM_xmlhttpRequestを渡していけない」*1などがいわれていましたが、実際にはこのように「unsafeWindowのプロパティにオブジェクトを登録する」、または「unsafeWindowの関数を呼び出す」だけで GM_xmlhttpRequest が漏洩してしまいます。
さらに性質の悪いことに、Firefoxではゲッタ/セッタの定義、や watch関数といったものもあるため、user.js側が関数を呼び出しているつもりが無くても攻撃者の関数が呼び出されてしまうことがあります。

このようにunsafeWindowは文字通り「unsafe」です。極力、使用は避けた方がよいでしょう。これは Greasemonekyのセキュリティホールなのか、それとも単なるuser.jsの書き方の問題なのか微妙ですが、user.jsを書く人(できれば使う人も)は気をつけた方がいいと思います。

なお、以下の操作は問題ないようです。*2

  • window.alert()など、unsafeWindowではなくwindowから関数を呼び出す。(攻撃者はそれらの関数を上書きできない。)
  • Stringオブジェクトなどの組み込み型の関数を呼び出す。(攻撃者が組み込み型のprototypeを書き換えてもuser.jsの側には影響しない)
  • DOM関係の関数を呼び出す。(それらの関数はラッパーに置き換えられているため、攻撃者が上書きしても影響されない。ただし、unsafeWindow.document.createElementのようにunsafeWindowから辿るのはNG。)

ちなみに今問題としているのは、主に上記の例のような @include に不特定多数のURLがマッチするような場合です。Livedoor ReaderGoogleの検索結果に機能を追加するようなuser.jsの場合は、そもそも元のページにXSS脆弱性がない限り攻撃者のスクリプトが紛れ込むことが無いので、unsafeWindowを使用していても特段の問題は無いでしょう。

*1:argumentsオブジェクトから呼び出し元を辿って参照できてしまうため。

*2:他は全部危険というわけではないです。試していないだけ。ちなみにdocument.titleの参照はゲッタが定義できるのでダメでした。