chromeのクロスドメインセキュリティチェック探訪

ブラウザにはjavascriptのコードがそのウインドウにアクセスすることができるか same origin policy に従ってチェックしている部分があります。この部分はセキュリティチェックと呼ばれています(FirefoxではnsScriptSecurityManager, WebKitではSecurityOriginで実装されている)。

Firefoxはjavascriptのオブジェクトひとつひとつに、そのオブジェクトを定義したページのドメインをもとにprincipalを設定し、実行時にアクセスしようとしているオブジェクトのprincipalと実行コンテキストのprincipalが一致するかをチェックすることで same origin policy を実装しています。Firefoxは実行コンテキストがネイティブコードや拡張機能由来のもののときには、常にprincipalが一致するものとみなすことでFirefoxの拡張機能から安全にウェブのコンテンツを操作することを可能にしています。拡張機能側のjavascriptからはprincipal(簡単にいえばドメイン)が異なっているページのオブジェクトでも操作できるようになっているのに対して、ページの側からは same origin policy がそのまま適用されるのでprincipalが異なっている拡張機能側のオブジェクトにはアクセスできません。これによってページの側から拡張機能でしか使えないような危険な操作を可能にするオブジェクトにはアクセスできなくなっています(詳しくはSecurity check basics 和訳)。

この仕組みのおかげでGreasemonkeyのようにコンテンツの中を操作する拡張機能をjavascriptで安全に記述することが可能になっています。
一方、SafariのベースになっているWebKitにはそのような仕組みがないため、GreaseKitGM_xmlHttpRequestのように信頼できないページからアクセスできてしまうと問題になるメソッドを実装したときに、ユーザスクリプトからはアクセスできるけどコンテンツのページ側からはアクセスできないようにすることができません。

google chrome inspectorの怪

google chromeはレンダリングエンジンはWebKitベースでjavascriptの実行エンジンはV8という独自実装なので、その辺どうなってるのかなとつついてみようと


<html><body>
<iframe src="http://mixi.jp/"></iframe>
</body></html>

というHTMLをローカルに置いてinspectorで

window.frames[0].document.body.innerHTML

にアクセスしてみたら、本当なら same domain policy にひっかかって見えないはずの異なるドメインを開いているウインドウの中身にそのままアクセスできたので、これはどういうことかと思ったのがきっかけです(ちなみに同じことをSafariのinspectorでやるとちゃんと same domain policy にひっかかるのでこのへんsafariとchromeで実装が異なるようです)。

はじめはchromeのセキュリティホール!と思ったけどそんなことはなくて、

window.frames[0].document.body.innerHTML

をページの中に埋め込んで実行するとセキュリティチェックに引っかかります。つまりページの中で実行した時には same origin policy にひっかかるけれど、chromeのinspectorから実行したときは same origin policy をパスできる何かがある可能性があります。inspectorからはoriginが異なる二つのページにアクセスできていることになるので、inspectorのコードを特別扱いさせている何かがわかれば、これを利用してFirefoxのように一方からは無条件にアクセスできるけど、他方からはアクセスできないオブジェクトを作ることが可能になり、chrome上にGreasemonkeyと同等のものを実装できるはずです。

と、ひとりで盛り上がったけどけっきょくなんでなのかまでつかめてません。以下WebKitのコードを追ったところ。
Safariもchromeもjavascriptで実装されているinspectorの

~/webkit/WebCore/page/inspector/Console.js
    _evalInInspectedWindow: function(expression)
    {
        if (WebInspector.panels.scripts.paused)
            return WebInspector.panels.scripts.evaluateInSelectedCallFrame(expression);
        return InspectorController.inspectedWindow().eval(expression);
    },

から始まって

~/webkit/WebCore/page/InspectorController.cpp
static JSValueRef inspectedWindow(JSContextRef ctx, JSObjectRef /*function*/, JSObjectRef thisObje
ct, size_t /*argumentCount*/, const JSValueRef[] /*arguments[]*/, JSValueRef* /*exception*/)
{
    InspectorController* controller = reinterpret_cast<InspectorController*>(JSObjectGetPrivate(thisObject));
    if (!controller)
        return JSValueMakeUndefined(ctx);

    JSDOMWindow* inspectedWindow = toJSDOMWindow(controller->inspectedPage()->mainFrame());
    JSLock lock;
    return toRef(JSInspectedObjectWrapper::wrap(inspectedWindow->globalExec(), inspectedWindow));
}

~/webkit/WebCore/bindings/js/JSDOMWindowBase.cpp
JSDOMWindow* toJSDOMWindow(Frame* frame)
{
    if (!frame)
        return 0;
    return frame->scriptProxy()->windowShell()->window();
}

ここのFrame::scriptProxyはjsオブジェクトとC++コードのブリッジになっているオブジェクト。

つながりがいまいちつかめなかったけどevaluateはこのメソッドを呼んでいるっぽい。

~/webkit/WebCore/bindings/js/kjs_proxy.cpp
JSValue* KJSProxy::evaluate(const String& filename, int baseLine, const String& str)
{
    // evaluate code. Returns the JS return value or 0
    // if there was none, an error occured or the type couldn't be converted.

    initScriptIfNeeded();
    // inlineCode is true for <a href="javascript:doSomething()">
    // and false for <script>doSomething()</script>. Check if it has the
    // expected value in all cases.
    // See smart window.open policy for where this is used.
    ExecState* exec = m_windowShell->window()->globalExec();
    m_processingInlineCode = filename.isNull();

    JSLock lock;

    // Evaluating the JavaScript could cause the frame to be deallocated
    // so we start the keep alive timer here.
    m_frame->keepAlive();

    m_windowShell->window()->startTimeoutCheck();
    Completion comp = Interpreter::evaluate(exec, exec->dynamicGlobalObject()->globalScopeChain(),
 filename, baseLine, StringSourceProvider::create(str), m_windowShell);

    m_windowShell->window()->stopTimeoutCheck();

    if (comp.complType() == Normal || comp.complType() == ReturnValue) {
        m_processingInlineCode = false;
        return comp.value();
    }

evaluateの引数は

~/webkit/JavaScriptCore/kjs/interpreter.cpp
Completion Interpreter::evaluate(ExecState* exec, ScopeChain& scopeChain, const UString& sourceURL
, int startingLineNumber, const UString& code, JSValue* thisV)

となっていた。ExecState, ScopeChainはjsの実行エンジンと関係ありそうだからそのへんなかんじもするけどここから先見てません。

chromeをビルドしてデバッガかけたらすぐわかりそうだけどビルド環境作るのめんどそうであきらめました。

補足 2008.10.29

その後わかりました。再度chromeのクロスドメインセキュリティチェック探訪

蛇足

chromeにはSandbox (Chromium Developer Documentation)というのがあって、FirefoxのGreasemonkeyでも同じ名前のものがあるので、もしかしてと思ったけど別物でした。chromeのsandboxはプロセスがOSのリソース(CPU, memory, I/O)にアクセスするのを制限するためのものでjavascriptとはぜんぜんレイヤが違っていて関係ないものでした。

このDesign Documents (Chromium Developer Documentation)は実装についてマンガより詳しく書いてあります。


About this entry