m2O

2011/05/19 (木)

Opera9.6 から「安全でなくなった」 UserJavaScript があるけどイコール「危険」というわけでもない話

|  Opera9.6 から「安全でなくなった」 UserJavaScript があるけどイコール「危険」というわけでもない話 - m2O を含むブックマーク


何を主張したいのか良くわからないタイトルだけど、以下のような話をします。

  1. 発端となる Opera9.6 の変更
  2. どんな UserJavaScript が「安全でない」か
  3. どんなリスクがあるか
  4. でも「危険」というわけでもないケースが多い
  5. @include の仕様と、できるだけ安全にするためのTips
  6. 特権コードを使う UserJavaScript はすべからく extension に移行すべき(審議中)

--

発端となる Opera9.6 の変更

d:id:os0x:20081120:1227165925 で2008年11月に既に紹介されているように、Function に caller プロパティが追加されました。

Function.caller

9.60 beta 1 RCからFunctionにcallerプロパティが追加され、関数の呼び出し元を参照できるようになりました。

arguments.callee.caller で UserJavaScriptのソース取得 - os0x.blog

このプロパティから、Function の呼び出し元の Function を取得することが出来ます。

function hoge() {
  alert(hoge.caller);
}
hoge(); // null (グローバル領域で実行した場合)
(function aa() { hoge(); })(); // function aa() { hoge(); }

これは JavaScript の機能追加であって、一般的な JavaScript 処理系で問題が起こることはありません。

ですが Opera の UserJavaScript ではページ側の script と UserJavaScript 側で同じ Object(window や document、その他全てのビルトインオブジェクト) を共用しているため、ページ側の script では許されていない特権的な処理をUserJavaScript でやっていると、ページ側の script からそれらのオブジェクトを経由して、その特権的な処理を乗っ取ることが出来てしまいます(回避方法はありますがここでは触れません)。

// UserJavaScript でクロスドメイン通信を行う function 
function xGet(url, callback) {
    var s = document.createElement('script');
    ... snip ...
}
// ページ側から document.createElement を書き換えることで UserJavaScript の xGet を取得する。
var evilXGet = null;
var _org_createElement = document.createElement;
document.createElement = function(){
    if (!evilXGet && /xGet/.test(arguments.callee.caller)) {
        evilXGet = arguments.callee.caller;
        document.createElement = _org_createElement;
    }
    if (evilXGet) {
        // ためしに Google トップページを取得。
        evilXGet('http://www.google.co.jp/', function(txt) { alert(txt.substring(0, 200)) });
    }
    return _org_createElement.apply(document, arguments);
}

Opera の UserJavaScript と同様な機能を提供する Firefox のアドインの GreaseMonkey では、window や ビルトインオブジェクトは全て別のオブジェクトに置き換えられ(ラップされ)ていて、使い方を間違えなければ安全に GM_xmlhttpRequest などの特権コードを使うことが出来ます。


どんな UserJavaScript が「安全でない」か

まず、ほとんどの UserJavaScript は安全です。

UserJavaScript はもともと「Opera というマイナーブラウザでうまく動かない(表示できない)WEBサイトを閲覧者側で何とかする」という目的で導入されました。( ユーザー JavaScript にて使用可能なメソッドとイベントAPI 郡から推測。)

ですから、ページ側の script で出来ること以上のことは基本的にできないようになっています。

例えば以下のような UserJavaScript が考えられますが、どれもページ側の script でもできることなので安全です。

  • はてブ数を表示する(img 要素を追加しているだけ)
  • リファラが Google の検索結果だったらリファラから検索語を取得してページ内のその文字を強調表示する(リファラはページ側の script で取得可能。後は自ページの DOM を操作するだけ)。
  • ニコニコ動画のどのページでもマイリストを操作する(同じオリジンのサーバーと通信)。

では「安全でない」とはどういったケースでしょうか。このエントリを書いている時点でわかっているのは以下の2ケースです。

  • UserJavaScript 内にユーザーIDやパスフレーズなど、個人情報や特別な処理を行うための情報を直接記述している。(前述の d:id:os0x:20081120:1227165925 で指摘されているもの。)
  • UserJavaScript固有の機能を利用して、別オリジンのサーバーと通信できるようにしている。(http://mattz.xii.jp/comment/60#comment-60id:edvakf が指摘しているもの。)

どんなリスクがあるか

UserJavaScript 内にユーザーIDやパスフレーズを直接記述した場合、それらが盗まれる危険性があります。

上記のような UserJavaScript は滅多に無いと思いますが、例としてあげるなら私の Auto Commit del.icio.us(紹介ページ) があります。
もしパスフレーズが盗まれると、悪意のページを開いただけで delicious にブックマークが追加されてしまいます。

このケースを見ただけではリスクはとても小さく見えますが、そのユーザーID/パスフレーズによって何が出来るかが、そのままリスクの大きさになります。


別オリジンのサーバーと通信する処理が乗っ取られると、あなたがログインしているWEBサイトのページがその悪意のページに読み取られることになります。

もっともシンプルな例を挙げると Google のトップページです。GMail にログインしていると右上にメールアドレスが表示されますよね。これが読み取られますから悪意のページからあなたの GMail アドレスがわかってしまいます。

最悪のケースでは、あなたがログインしているWEBサイトでパスワードの再入力無しにできることが、以下のシナリオで行われる可能性もあります。

  1. 何らかの処理(例えば退会処理)を行うページを読み取る。
  2. 読み取ったページのソースから、処理を行うためのパラメーター(ワンタイムトークン等)を取得する。
  3. 処理を行うURLに上記のパラメーターを含めてリクエストすると処理が完了する。

ただし、紹介したシナリオで、退会処理のように重要な処理が行えることはまずありません。
これは以下の理由によるものです。

  • 重要な処理を行うページは通常 https で提供されます。悪意のページは当然別ドメインですから、別ドメインから https のページにリクエストがあると Opera ブラウザは警告を表示します。
  • 重要な処理の場合、いまどきのWEBサイトではパスワードの再入力を求めます。パスワードがわからないかぎり、悪意のページは処理を実行することは出来ません。


でも「危険」というわけでもないケースが多い

「安全でない」 UserJavaScript がほとんど無いということは既にお話しましたが、さらに UserJavaScript が安全でなかったとしてもすなわち「危険」でもないということをお話します。

良識ある UserJavaScript の作者の方は、その UserJavaScript が実行されるページを @include で設定します。

// ==UserScript==
// @name          Name of UserJavaScript
// @include       http://www.google.com/search?*
// @include       http://www.google.co.jp/search?*
// ==/UserScript==

この例では Google の検索結果のページでしかこの UserJavaScript が読み込まれないことを保障します。つまり上で散々危険性を煽った「悪意のページ」の所業ですが、この UserJavaScript においては Google にしか行うことが出来なくなります。

もちろん UserJavaScript の実行を許可しているページに XSS 脆弱性があったり、(https でないと)ページが改竄されたりすることが全くありえなくも無く、その場合は前提が崩れてしまいますが、その場合はもっとひどいことが起こっていますので、多くの人が使用する有名な UserJavaScript でない限りは攻撃対象として狙われることはないと思われます。

ですから、あなたが UserJavaScript を使用する場合、どのスクリプトに対しても1度、使用するかどうかを判断する必要があるでしょう。

  • その UserJavaScript が「安全」(あなたが個人情報やパスワードを入力する必要が無い、別のWEBサイトと通信を行わない)かどうか。
  • 「安全でない」場合、@include で適用されるWEBサイトを、前述のリスクを踏まえて信用するかどうか。

先に紹介した「Auto Commit del.icio.us」のケースでは、パスフレーズを盗めるページがまさに「del.icio.us」本体なので、「安全で無い」けれども「危険で無い」典型的な例ですね。


@include の仕様と、できるだけ安全にするためのTips

以前、こんなエントリを書きました。

こんなスクリプトを書いたんだけど、

// ==UserScript==

// @name script - prevent external script

// @description prevent external script by hostname of page.

// @include http://codezine.jp/

// ==/UserScript==

(function(){

window.opera.addEventListener('BeforeExternalScript',function(e){

e.preventDefault();

},false);

})();

これだと http://b.hatena.ne.jp/entry/http://codezine.jp/ なんかでも適用されて困った。

ユーザーJavaScript で @include する時は、他のURLを含むURLに注意 - m2O - チーム俺等

実はこの問題は既に改善されていて、

// @include http://codezine.jp/

この場合は文字通り「http://codezine.jp/」のトップページでしか動かなくなっています。

他のページでも適用するには以下のようにワイルドカードを使用します。

// @include http://codezine.jp/*

ただ、依然として問題は残っていて、ワイルドカードがホントに何でもアリなところです。

例えば、Google 検索と Google Map で使いたい場合、

// @include http://*.google.co.jp/*

のような指定をしてしまうと、http://www.evil.com/?.google.co.jp/ のような URL でも動いてしまいます。

以下のように、面倒でも後方にだけワイルドカードをつけるようにすると安全です。

// @include http://www.google.co.jp/*
// @include http://map.google.co.jp/*

特権コードを使う UserJavaScript はすべからく extension に移行すべき

(審議中)

トラックバック - http://orera.g.hatena.ne.jp/miya2000/20110519