フロントエンドWeb戦略室

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

この記事を読むのに必要な時間:およそ 4 分

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

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

もし外部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を使ったクロスドメイン通信を利用する必要があるでしょう。

注9)
対応しているブラウザはInternet Explorer 8以上,Firefox 3以上などです。

著者プロフィール

mala(マラ)

NHN Japan所属。livedoor Readerの開発で知られる。JavaScriptを使ったUI,非同期処理,Webアプリケーションセキュリティなどに携わる。

Twitter:@bulkneets