フロントエンドWeb戦略室

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

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

安全に提供するために──プライバシー編

SNSSocial Networking Serviceにリンクを共有するためのボタンや,言及数を表示するカウンタ画像のプライバシー問題について取り上げます。

ソーシャルボタンがサイトに設置されていれば,1クリックで気軽にシェアできたり,ツイートできたりします。こういった機能は非常に有用ですが,一方で大量のユーザを抱え,個人情報を預かるサービス事業者が考えなければならないことは多いのです。この節ではブログパーツやソーシャルボタンを例に挙げて解説しますが,大量のユーザ情報を抱えないサービスでも参考になることが多いと思います。一緒に確認していきましょう。

ブログパーツやソーシャルボタンを作成する側の心得

常時ログインしている大量のユーザを抱え,個人を特定できる情報を預っているサービス事業者にとって,Cookieを送信するドメインのiframeを埋め込むことは,深刻なプライバシーリスクとなります。個人を識別可能なCookieを受け取りつつ,リファラによって埋め込まれた対象のURLを取得することも可能であるため,広範なWebサイトから訪問履歴を取得可能な状態になってしまうからです。こういったボタンはその性質上,リクエストされたURLを集計することで訪問者のWeb閲覧履歴を収集することも可能です。

画像やJavaScriptファイル,iframeなど,外部のリソースを読み込むのであれば,それだけでアクセスログが残ることになります。これらをログインCookieを持っているドメインで提供する場合,サービス提供者がその気になれば,サービス利用者が外部でどんなサイトを訪問したのか,Cookieの情報を使って高い精度で把握できる状態になります。

もちろん,Cookieの情報をわざわざ参照せずに,誰が訪問したのか把握しないことも可能です。しかしユーザや設置するWebサイト側から見た場合,サーバ側でどのようにログが記録されているのかは確認しようがないのです。

誰が訪問したのかに特別な意味があるケース

たとえば,Facebookのいいね!ボタンは,iframe上に同じURLをいいね!した友人の一覧を表示する機能を持っています。Googleの+1ボタンも外部サイト上でのパーソナライズを有効にした場合,同等の機能を持ちます。このようにiframeにアクセスした段階で「誰が訪問したのかを識別する必要がある」または「iframe上で何らかの操作を直接実行する」場合は,ログイン状態の取得が必要です。そのような機能に必然性がない,つまり「誰に対しても同じレスポンスを返す」ようなウィジェットであるならば,ログインCookieを持たないドメインでホスティングしたほうが,「トラッキングに使われているのではないか?」といった余計な疑いが掛からなくて済みます。

また,外部サイトにログイン状態のiframeを埋め込み,さらにユーザの確認なしに何らかの操作を行う場合,ユーザが特にセキュリティ対策のためのブラウザ拡張機能などを導入していない限りは,クリックジャッキング攻撃注10を防ぐことができません。

iframe上での操作は「勝手に行われても差し支えない範囲」にとどめることが肝要です。

GoogleやFacebookはその気になれば,認証Cookieを持たないドメインでソーシャルボタンを提供することもできたはずです。また「同じURLをいいね!した友人の一覧」などログイン状態が必要になる機能を提供するのであれば,その機能を必要とするユーザにだけオプトインで提供することも可能だったはずです。

注10)
Webページ中にスタイルシートで透明状態にしたiframeを埋め込み,ユーザに気づかれずにiframeに対して操作を行わせる攻撃手法です。

有効化するまでアクセスログを残さないソーシャルボタンを作る

ここではさらに一歩進んで,ユーザが明示的に許可するまでは一切アクセスログを残さないソーシャルボタンの実装を考えてみます。アクセスログを一切残さないために,次の4つの方法が考えられます。

  • a リファラで埋め込み先のURLがわかるのを防ぐ
  • b 共有対象のURLはクエリパラメータで送る代わりに,location.hashやpostMessageで送る
  • c ユーザが明示的に有効化するまで動作しないようにする
  • d 必要であればURLをハッシュ化して使用する

実際にはこれらを組み合わせて使います。

実装例ですが,JSONP APIの例と同じように,iframeをサンドボックスとして使います。iframeを埋め込む親フレームリスト3と,埋め込まれる子フレームリスト4を参照してください。

リスト3 socialbutton.html

<div id="social_button"></div>

<script>
function show_social_button(target_id, target_url){
    var i = document.createElement("iframe");
    i.style.border = "none";
    i.width = "300";
    i.height = "25";
    i.scrolling = "no";
    document.getElementById(target_id).appendChild(i);
    var social_proxy = "http://localhost/socialbutton_proxy.html";
    var target = encodeURIComponent(target_url);
    if(/webkit/i.test(navigator.userAgent)) {  ←(1)
        i.contentWindow.document.write("<script>location.href='" +
        social_proxy + "#" + target + "'<" + "/script>");
    } else {
        i.contentWindow.document.write('<meta http-equiv="refresh"
        content="0;url=' + social_proxy + "#" + target + '">');
    }
}
show_social_button("social_button", "http://www.livedoor.com/")
</script>

リスト4 socialbutton_proxy.html

<!DOCTYPE html>
<html>
<head>
    <title>socialbutton proxy</title>
    <style>
    body{margin: 0; padding: 0}
    </style>
</head>
<body>
<button onclick="toggle()" id="switch">show social button</button>
<span id="button"></span>
<script>
(localStorage.enable == 1) ? show_button() : hide_button();
function toggle(){  ←(3)
    localStorage.enable = (localStorage.enable == 1) ? 0 : 1;
    (localStorage.enable == 1) ? show_button() : hide_button();
}
function show_button(){
    var target_url = decodeURIComponent(location.hash.substring(1));  ←(2)
    var img = document.createElement("img");
    img.src = "http://image.clip.livedoor.com/counter/" + target_url;
    var b = document.getElementById("button");
    b.innerHTML = "";
    b.appendChild(img);
    b.style.display = "inline";
    document.getElementById("switch").innerHTML = "hide social button";
}
function hide_button(){
    document.getElementById("button").style.display = "none";
    document.getElementById("switch").innerHTML = "show social button";
}
</script>
</body>
</html>
リファラで埋め込み先のURLがわかるのを防ぐ

JavaScriptやiframeが埋め込まれているページのURL自体を秘匿(ひとく)できます。実装例では,リスト3(1)でリファラを残さないように読み込んでいます。

リスト4の実装例は,サービス提供者がログを取得できない第三者に設置してもらうことを想定したものです。これは静的なファイルを長期間ブラウザにキャッシュすることによっても実現できます。リンク先にリファラを送信しないための属性,rel="noreferrer"というのがWebKitでサポートされていますが,これは現状iframeに対しては使えないようです注11)。

注11)
<meta name="referrer">という仕様も提案されており,これはページ内に含まれるリンクに対して一括でポリシーを設定可能にするものです。SNSにおけるユーザーIDや検索エンジンにおける検索キーワードなど,URL内に外部に知らせたくない文字列が含まれている場合に利用することができるでしょう。将来的には特定の外部リソースに対してリファラを送らないような仕様が制定される可能性もありますが,現状では特定のiframeに対してリファラを送らないという実装が正攻法では不可能であるため,iframe内からリダイレクトするという手法を使っています。本稿執筆時点でGoogle Chromeのみサポートしています。
http://wiki.whatwg.org/wiki/Meta_referrer
共有対象のURLをクエリパラメータで送らない

ソーシャルボタンが設置される対象の記事のURLが,URLの?以降のパラメータで送られるためアクセスログに残ってしまうのを防ぎます。共有対象のURLをlocation.hashで受け渡すことでアクセスログに残さないようにすることが可能です。リスト4(2)が該当の個所です。

有効化するまで動作しないようにする

これは特に重要なポイントです。クッションとなる第三者のドメイン上でソーシャルボタンの表示するかどうかの設定を切り替えています。localStorageに設定を保存して,無効の場合は永続的にボタンを表示しないようにしているのです。

リスト4のサンプルではデフォルトで無効にしていますが,デフォルト有効でも一度設定すればlocalStorageが消去されるまで設定が保存されます。無効の場合はリクエスト自体を行いません。つまり,どのURLを表示していたのかを外部のサーバに一切送りません。リスト4(3)が該当の個所です。

既存の画像やiframeを直接埋め込む方法では,ユーザが機能自体を無効化したいと考えても,表示した段階でリクエストが行われてしまうため,アクセスログに残ることを防げませんでした。サービス事業者を信用していない,アクセスログに残ることすら問題がある,と考える場合は,ブラウザ側で拡張機能を使ったり,コンテンツブロックの指定をする必要がありました。そのような拡張機能がないモバイルブラウザであれば,ユーザはまったく防ぐことができません。

URLをハッシュ化して使用する

リスト3~4の実装例では触れていないのですが,これはたとえ未知のURLがサービス提供者に送られても,元のURLがわからないようにするためのしくみです。もしブラウザ拡張機能やツールバーを使って訪問したすべてのURLに対して言及数を調べるような機能を付けるのであれば,こうしたAPIを備えることを強く推奨します。なぜならURLにセッションIDやアクセストークン,パスワードが含まれるといった状況が多々あるからです。サービス事業者にとって既知のURLであれば元のURLがわかりますが,未知のURLであればユーザがどのサイトに訪問したのかわからないという状況を作ることができます。DeliciouslivedoorクリップはURLのハッシュ値で言及数を取得するAPIを持っています。この方法のデメリットは,リダイレクト先を追いかけたり,サーバサイドでURLの複雑な正規化を行うことができなくなることです。

ブラウザ拡張機能を使わずにソーシャルボタン

さて,こういった手順を取ることで,ブラウザ拡張機能を使わなくても,ユーザが明示的に有効化するまで訪問サイトのアクセスログを残さないソーシャルボタンを作ることができます。これは単なるコンセプトですが,location.hashやlocalStorageといった「サーバにログを残さない」機能を使うことで,ユーザが明示的に有効化するまで,サーバに今見ているURL情報を送らない,というのは案外簡単に実現できることがおわかりいただけたかと思います。

リファラ送信の抑止はややバッドノウハウですが,広範に埋め込まれるソーシャルボタン,ウィジェット,画像といったものについて「トラッキングを意図しない」「むしろなるべくアクセスログですら情報を預かりたくない」という需要がある程度あるならば,検討する価値があるでしょう。

今回のまとめ

今回は広告やブログパーツにおけるJavaScriptについて紹介しました。提供する側は必然性がないのであれば,iframe内でも実行可能なように提供したほうがよいでしょう。

外部サイトにJavaScriptを直接埋め込んでもらう場合は,グローバル変数を汚染しないように配慮したり,読み込みが遅れてもページレンダリングを妨げないように慎重に設計する必要があります。

設置する側は保護すべき情報が何かを見極めて,外部から提供されるスクリプトが安全かどうか判断する必要があります。外部のJavaScriptを直接埋め込むことによって,ただちに何か危険が生じるということはめったにありませんが,それは単にサイト間の信用関係によって成り立っているだけで,「技術的に安全であると保証されている」わけではありません。将来的には,XMLHttpRequest level2 + CORSによって実行コード部分とデータを分離した安全なマッシュアップを実現したり,iframeとpostMessageを通じて必要な情報のみを受け渡すようにすることで,JavaScriptを直接実行するリスクを避けることができるでしょう。変化はゆっくりと起こります。本稿がその助けとなれば幸いです。

サードパーティドメインのlocalStorageは使用してよいのか?

サードパーティのドメインのlocalStorageは,ブラウザの設定によっては読み書きできないことは注意すべき点です。

Google ChromeではサードパーティCookie無効時には,サードパーティのlocalStorageにも制限がかかるようになりました。

iPhoneの標準ブラウザでもあるSafariではデフォルト設定でサードパーティCookieをブロックする設定になっていることで知られています。そのため,特にスマートフォン向けに,Cookieが使えない場合にlocalStorageを代替Cookieとして使うというテクニックがすでに使われてしまっています。

Cookieが使えない場合に(ユーザがブロックしていたとしても)代替Cookieとしてlocal Storageを使ってトラッキングをする,という「悪用」方法がある一方で,localStorageを使うことで,よりプライバシーに配慮した設計にすることも可能になります。localStorageはCookieと異なりサーバに勝手に送信されることがありません。この性質を利用することで,プライバシー保護のためのしくみに用いることができます。

ユーザにとって有益な使い方が多いのか,望まれない使い方が多いのかによって,今後のブラウザのポリシーは変化する可能性があります。

著者プロフィール

mala(マラ)

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

Twitter:@bulkneets

コメント

コメントの記入