フロントエンドWeb戦略室

第1回外部サイトに貼り付けるJavaScriptの作法―ポリシー、速度、セキュリティ、プライバシー(2)

速度に影響を与えないために

まずは外部サイト向けのJavaScriptが、そのサイトの読み込み速度へ与える影響を考えてみます。

Webページのレンダリングをブロックしないようにする

特別な配慮なく外部JavaScriptを提供すると、読み込み速度に影響を与えてしまいます。

外部JavaScriptの読み込みが遅れた場合、ページ全体の描画が停止してしまうのです。これはdocument.writeなど、読み込んだページに対してHTMLを出力するような命令が含まれていた場合には、ページ全体のレンダリング結果に影響を与えてしまうためです。ブラウザはscriptタグが記述されている順番でJavaScriptを実行しなくてはいけません[6]⁠。これを回避するためには、次の2つの戦略が考えられます。

defer属性やasync属性を活用する

Webページのレンダリングをブロックしないようにする1つ目の戦略は、あらかじめブログパーツやウィジェットを描画するエリアを指定しておいて、スクリプトのロードが終わりしだいコンテンツを描画する方法です。あるいはscriptタグの中で、appendChildやinsertBeforeを使って動的に描画エリアを作成するのもありでしょう。

scriptタグにdefer属性を付けると、そのスクリプトはページロード完了後に「遅れて」実行されます。

async属性を付けた場合は、スクリプトのロードが完了したタイミングで実行されます。async属性を使う場合、JavaScriptがどの順番で読み込み/実行が行われても影響がないように記述しなくてはいけません。

このように記述することで、もしスクリプトのロードが遅れても、ページ全体の描画をブロックすることがなくなります。ブログパーツやウィジェットであれば、この方法をお勧めします。ただしこの方式を採用する場合はスクリプト中でdocument.writeを使用すべきではありません。document.writeが含まれていてもスクリプトは実行されますが、Webページのレンダリングが完了した後にスクリプトが読み込まれた場合、document.writeは表示されたWebページの内容をクリアして、真っ白な状態のページに文字列を出力することになります。

document.write+ 追加のスクリプト

2つ目は、ローダとなるスクリプトと動的に生成されるスクリプトを分ける手法です。ブラウザは2段階に分けてスクリプトをロードします。defer属性やasync属性を使う方法と比べ、より幅広いブラウザをサポートしなくてはならないケースや、高速にレスポンスを返すことが保証できる場合は、この方法が選択肢となります。次の2段階に分けてスクリプトをロードさせます。

  • ① ブラウザに長期間キャッシュされる、ローダとなるjsファイル
  • ② ブラウザにキャッシュさせず、動的に生成されるjsファイルやiframe

これは広告配信などでよく使われる手法です。①については高速で読み込まれることを保証したうえでdocument.writeを使用し、その中から追加で読み込まれるjsファイルや、②の動的に生成されるコンテンツを追加でロードするようにします。

DOMDocument Object Modelをサポートしない古いブラウザ[7]をサポートする必要がある場合、document.writeを使用しなくては動作しないことになります。しかし、そのような古いブラウザは現在ではサポート期限が終了していたり、セキュリティパッチが提供されていないなどの問題があるため、使用すること自体が望ましくありません。よって今ではdocument.writeに依存する必然性はほとんどありません。

安全に提供するために─⁠─セキュリティ編

速度への影響がクリアになったところで、次に外部JavaScriptを安全に提供するためのセキュリティを考えます。具体例を見ていきましょう。

サンドボックスドメインを使う

外部JavaScriptのすべてが安全かどうか、利用者が確認・判断するのは容易ではありません。また「今は安全」と判断できても将来的に書き換えられてしまうことも考えられます。

外部に起因するJavaScriptを実行するなら、安全かどうかを慎重に確認するよりも、まずは「別ドメインで」実行することを考えましょう。検索エンジンやCGMConsumer Generated Mediaサイトなど、多くのサービスは任意のコンテンツをホスティングするための「サンドボックスドメイン」注8を持っています。

サンドボックスドメインとログイン情報を持つドメインを切り分けることができれば、セキュリティ上かなりのメリットがあります。任意のJavaScriptが実行されたとき、サンドボックスを使う場合と使わない場合でどのような差が生じるのか、表1にまとめました。

表1 任意のJavaScriptが実行された場合に何が起きるか
実行される場所何が起きるか
サンドボックスドメインの場合悪意のあるサイトへのリダイレクト、偽コンテンツの表示、ログインの有無に関係のない危険な操作
ログイン情報を持つドメインの場合そのドメインで行えるあらゆる操作

パスワード入力などの本人再確認がない範囲

サンドボックスドメインの例

たとえばlivedoorのサービスであれば、*.livedoor.comは主にlivedoorが提供するコンテンツで、ユーザがタグを自由に書くことはできません。一方、blog.livedoor.jpであれば、自由にHTMLタグを書くことができます。管理画面を提供するドメインと、ユーザが自由にコンテンツを置ける「表示用のドメイン」を分けているのです。

サンドボックスドメインの中では自由にJavaScriptを実行できるため、ブラクラや悪意のあるサイトへのリダイレクトを行うことができますが、ブログ管理用のドメインとはドメインが分かれているため、アカウントの乗っ取りなどを行うことができません。検索エンジンのキャッシュなどをホスティングしているドメインも、サンドボックスドメインと見なすことができます。

サンドボックスドメインを使わない場合

ブログサービスによっては、表示用のドメインと管理用のドメインが同一で、サービス事業者が許可したブログパーツの貼り付けのみ許可しているようなケースもあります。

このケースでは、ブログサービス事業者によって審査されたうえ、安全であると認められたブログパーツのみが利用可能です。しかし、ブログパーツやウィジェットの提供元が著名なサービスであるからといって安全だとは限りませんし、審査した段階では安全でも、ある日突然危険になる可能性もあります。この点、サンドボックスを使わないブログサービスは、利用者に観察力が求められています。

信頼できないブログパーツを貼り付けるには?

このように、セキュリティを考えるうえで、ブログパーツを貼り付ける側の立場で考えることも重要です。

もし外部JavaScript利用者がそのドメイン上で認証Cookieを持っている場合や、改変されることで重大な危険性を及ぼす場合には、そもそも外部のJavaScriptを一切読み込まないほうが望ましいです。

しかし広告やアクセス解析などで、外部のJavaScriptを一切貼り付けないということが現実的ではない場合もあるでしょう。その場合、基本的には次のようなポリシーで対応すればよいと思います。

  • ① iframeの中に表示させられないかを検討する
  • ② 無理であればソースを検証し、提供元が信用できるか審査したうえで利用する

このポリシーに則って、HTML5のsandbox属性を使う方法、もしくはJSONPの外部APIを安全に使う方法をお勧めします。

HTML5のsandbox属性を使う

HTML5であれば、iframeを使って次のような方法で貼り付けるのがよいでしょう。

iframe内でスクリプトを実行した場合でも、親フレームに干渉できなくなるだけでスクリプトの実行自体は行うことができます。たとえ元からiframeで提供されるブログパーツであっても、ページを丸ごと別のサイトへリダイレクトしたり、ブラウザやプラグインの脆弱性を利用されるなど、実行されるドメインに関係なく危険な操作が行われた場合、訪問者が危険にさらされることになります。

iframe内のスクリプトが実行可能な処理を制限できるよう、HTML5ではiframeにsandboxという属性が追加されました。sandbox属性を使うことで、iframe内でのスクリプトの実行を禁止できるようになりました。

スクリプトを使わないプレーンなHTMLでお知らせなどを配信したい場合、sandboxに対応したiframeで提供することで、設置サイト側が安心してブログパーツを貼り付けられるようになります。

JSONPの外部APIを安全に使うために

JSONPJSON with paddingとは、任意のcallback関数付きのJSONJavaScript Object Notationが記述されたJavaScriptコードを、scriptタグの動的追加によって呼び出す手法です。クロスドメイン通信やマッシュアップの手法として広く用いられています。

JSONPはscriptタグの追加によって外部から提供されるJavaScriptを直接実行するしくみになっています。そのため、JSONPのAPI提供者に悪意があった場合、あるいは何らかの脆弱性を突かれてJSONの内容が書き換えられていたり、通信経路での改ざんやDNSの書き換えなどが行われている場合、任意のJavaScriptコードが実行されることになり、安全に利用することができません。

JSONPのAPIをサンドボックス内で呼び出す

JSONPのAPIは、将来的にはXMLHttpRequest level2によってサポートされるクロスドメイン通信と、JSON.parseによって安全に置き換えることが可能です。

しかし現状、利用するAPIがクロスドメイン通信の仕様・CORSCross-Origin Resource Sharingsに対応していない場合は、JSONPのAPIをサンドボックス内で呼び出すことで、安全に呼び出すことができます

サンドボックスドメイン上では任意のJavaScriptコードが実行される可能性もありますが、API利用ドメインではJSONデータの受信しか行わないことが保証できます。図1は普通のJSONP APIのイメージ、図2が今回取り上げているサンドボックス内でJSONPAPIを呼び出すイメージです。

具体的な実装方法を解説します。ここではiframeをサンドボックスとして使います。iframeを埋め込むのがリスト1の親フレームです。ここで、任意のJavaScriptを実行されても差し支えのないサンドボックスドメインへjsonp_sandbox.htmlリスト2を設置し、iframeで読み込みます。親フレームはpostMessageでサンドボックスに対してリクエストするJSONPのAPIを指定し、サンドボックスはpostMessageで親フレームにレスポンスを返します。

図1 サンドボックスを使わない場合のJSONP API
図1 サンドボックスを使わない場合のJSONP API
図2 サンドボックスを使う場合のJSONP API
図2 サンドボックスを使う場合のJSONP API
リスト1 jsonp.html
<!DOCTYPE html>
<html>
<head>
    <title>JSONP with sandbox</title>
</head>
<body>
<iframe src="http://sandbox.example.com/jsonp_sandbox.html"
id="jsonp_sandbox" style="display:none"></iframe>
<script>
var sandbox_domain = "http://sandbox.example.com";
var request_id = 0;
var callbacks = { };

window.addEventListener("message", function(event){
    if (event.origin !== sandbox_domain) return;
    var data = JSON.parse(event.data);
    callbacks[data.request_id] &&
    callbacks[data.request_id] (data.response);
    delete callbacks[data.request_id];
});

function JSONPRequest(url, callback) {
    var jsonp = document.getElementById("jsonp_sandbox").content Window;
    callbacks[request_id] = callback;
    var request = {
        url: url,
        request_id: request_id++
    };
    jsonp.postMessage(JSON.stringify(request), "*");
}
function dump_jsonp(){
    var url = document.getElementById("url").value;
    JSONPRequest(url, function(res){
        document.getElementById("result").value =
        JSON.stringify(res);
    })
}
</script>
URL <input type="text" value="" id="url" size="80">
<button onclick="dump_jsonp()">GO</button><br>
<textarea style="width:100%; height:100px" id="result">
</textarea>
</body>
</html>
リスト2 jsonp_sandbox.html
<html>
<head>
<script src="http://code.jquery.com/jquery-1.7.1.js"></script>
<script>
window.onmessage = function(event){
    var data = JSON.parse(event.data);
    var request_id = data.request_id;
    delete data.request_id;
    data.dataType = "JSONP";
    data.success = function(res, status){
        var message = {
            request_id: request_id,
            response: res
        };
        event.source.postMessage(JSON.stringify(message), "*")
    };
    $.ajax(data);
};
</script>
</head>
<body></body>
</html>

これはあくまで、JSONPを安全に受け取る方法だということに注意が必要です。利用するAPIによっては、受け取ったAPIのレスポンスをそのままHTMLとして出力することが危険な可能性もあります。サンドボックス経由で読み込んだところで外部に起因する入力であることには変わらないので、信用せずにJavaScript側でHTMLエスケープしたうえで出力するか、textNodeとして出力する必要があります。

これは簡略化したサンプルですので、postMessageを使用できることを前提としています。postMessageに対応していないブラウザ[9]で動作させるためには、location.hashやwindow.nameを使ったクロスドメイン通信を利用する必要があるでしょう。

おすすめ記事

記事・ニュース一覧