ブラウザのセキュリティの基礎は「同一生成元ポリシー」です。多くの開発者はこのことを知っていますが、完全には理解していません。
概要
意味
1995 年に、同一生成元ポリシーが Netscape によってブラウザに導入されました。現在、すべてのブラウザがこのポリシーを実装しています。
当初の意味は、2 つの Web ページのオリジンが同じでない限り、Web ページ A によって設定された Cookie を Web ページ B で開くことはできないということです。いわゆる「同源」とは「3つの類似点」を指します。
契約内容は同じです
同じドメイン名
ポートは同じです (これは無視できます。詳細については以下を参照してください)
たとえば、http://www.example.com/dir/page.html
この URL の場合、プロトコルはhttp://
、ドメイン名はwww.example.com
、ポートは 、80
(デフォルトのポートは省略可能)、その相同性は次のようになります。
-
http://www.example.com/dir2/other.html
:同源 -
http://example.com/dir/other.html
: ソースが異なります (ドメイン名が異なります) -
http://v2.www.example.com/dir/other.html
: ソースが異なります (ドメイン名が異なります) -
http://www.example.com:81/dir/other.html
: 異なるソース (異なるポート) -
https://www.example.com/dir/page.html
: 異なるソース (異なるプロトコル)
標準では、異なるポートを持つ URL は同じ生成元ではない (たとえば、ポート 8000 とポート 8001 は同じ生成元ではない) と規定されていますが、ブラウザはこの規則に準拠していないことに注意してください。実際、同じドメイン内の異なるポートは、互いの Cookie を読み取ることができます。
目的
同一生成元ポリシーの目的は、ユーザー情報のセキュリティを確保し、悪意のある Web サイトによるデータの盗難を防ぐことです。
次の状況を想像してください: Web サイト A は銀行であり、ユーザーがログインすると、Web サイト A はユーザーのマシンに個人情報を含む Cookie を設定します。ユーザーが Web サイト A を離れた後、Web サイト B に再度アクセスすると、送信元制限がなければ、Web サイト B は Web サイト A の Cookie を読み取ることができ、プライバシーが漏洩します。さらに恐ろしいのは、ユーザーのログイン状態を保存するために Cookie が使用されることが多く、ユーザーがログアウトしないと、他の Web サイトがユーザーになりすまして、やりたいことが何でもできるようになるということです。ブラウザーは、フォームの送信が同一生成元ポリシーによって制限されないことも規定しているためです。
同一生成元ポリシーが必要であることがわかります。そうでないと、Cookie が共有される可能性があり、インターネットはまったく安全ではなくなります。
限界範囲
インターネットの発展に伴い、同一生成元ポリシーはますます厳しくなっています。現在、同じ起源でない場合に制限される動作が 3 つあります。
(1) オリジナル以外の Web ページの Cookie、LocalStorage、IndexedDB は読み取れません。
(2) 異質な Web ページの DOM にはアクセスできません。
(3) AJAX リクエストを元のアドレス以外に送信することはできません (送信することはできますが、ブラウザは応答の受け入れを拒否します)。
さらに、他のウィンドウのオブジェクトは JavaScript スクリプトを通じて取得できますwindow
。オリジナルでない Web ページの場合、ウィンドウは現在、window
他の Web ページのオブジェクトの 9 つのプロパティと 4 つのメソッドにアクセスできます。
-
窓が閉まっている
-
窓枠
-
ウィンドウの長さ
-
窓の場所
-
ウィンドウオープナー
-
window.parent
-
window.self
-
ウィンドウトップ
-
窓.窓
-
window.blur()
-
window.close()
-
window.focus()
-
window.postMessage()
上記の 9 つの属性のうち、読み取りおよび書き込みが可能なのは 1 つだけでwindow.location
、他の 8 つはすべて読み取り専用です。また、location
オブジェクトが同じ起源のものでなくても、メソッドの呼び出しlocation.replace()
とlocation.href
プロパティの書き込みのみが許可されます。
これらの制限は必要ですが、場合によっては不便になり、正当な目的が損なわれてしまいます。上記の制限を回避する方法は次のとおりです。
クッキー
Cookie はサーバーによってブラウザに書き込まれる小さな情報であり、同じオリジンを持つ Web ページ間でのみ共有できます。2 つの Web ページの第 1 レベルのドメイン名が同じで、第 2 レベルのドメイン名だけが異なる場合、ブラウザではdocument.domain
設定を通じて Cookie の共有が許可されます。
たとえば、Web ページ A の URL が で、http://w1.example.com/a.html
Web ページ B の URL が の場合http://w2.example.com/b.html
、設定が同じである限りdocument.domain
、2 つの Web ページは Cookie を共有できます。ブラウザはdocument.domain
属性を通じて同じ起源を持つかどうかをチェックするためです。
// 両方の Web ページで document.domain = 'example.com' を設定する必要があります。
Web ページ A と B の両方がdocument.domain
同じオリジンを達成するために属性を設定する必要があることに注意してください。document.domain
設定時にポートがリセットされるためnull
、Web ページを 1 つだけ設定した場合document.domain
、2 つの URL のポートが異なり、相同性の目的が達成されません。
ここで、Web ページ A はスクリプトを介して Cookie を設定します。
document.cookie = "test1=hello";
Web ページ B はこの Cookie を読み取ることができます。
var allCookie = document.cookie;
このメソッドは Cookie および iframe ウィンドウにのみ適用されることに注意してください。LocalStorage および IndexedDB は、このメソッドでは同一生成元ポリシーを回避できません。代わりに、以下で紹介する PostMessage API を使用してください。
さらに、サーバーは、Cookie を設定するときに、Cookie のドメイン名を第 1 レベルのドメイン名として指定することもできます (例: ) example.com
。
Set-Cookie: キー=値; ドメイン=example.com; パス=/
この場合、第 2 レベル ドメイン名と第 3 レベル ドメイン名の両方が設定なしでこの Cookie を読み取ることができます。
iframe とマルチウィンドウ通信
iframe
要素は、現在の Web ページ内の他の Web ページに埋め込むことができます。各iframe
要素は独自のウィンドウを形成します。つまり、独自のwindow
オブジェクトがあります。iframe
ウィンドウ内のスクリプトは親ウィンドウと子ウィンドウを取得できます。ただし、親ウィンドウと子ウィンドウが通信できるのはオリジンが同じ場合のみで、ドメインを越えた場合は相手のDOMを取得できません。
たとえば、親ウィンドウが次のコマンドを実行する場合、iframe
ウィンドウが同じソースからのものでない場合、エラーが報告されます。
document .getElementById("myIFrame") .contentWindow .document // キャッチされない DOMException: フレームのクロスオリジン フレームへのアクセスがブロックされました。
上記のコマンドでは、親ウィンドウは子ウィンドウの DOM を取得しようとしていますが、クロスドメインのためエラーが報告されます。
逆に、子ウィンドウもメイン ウィンドウの DOM を取得するときにエラーを報告します。
window.parent.document.body // エラーレポート
この状況はウィンドウだけでなくiframe
、メソッドによって開かれたウィンドウにも当てはまりwindow.open
、ドメインを越える限り、親ウィンドウと子ウィンドウの間で通信は行われません。
2 つのウィンドウの第 1 レベルのドメイン名が同じで、第 2 レベルのドメイン名だけが異なる場合は、前のセクションで紹介した属性を設定することで、document.domain
同一生成元ポリシーを回避して DOM を取得できます。
まったく異なる起源を持つ Web サイトの場合、クロスドメイン ウィンドウの通信の問題を解決するには、現在 2 つの方法があります。
フラグメント識別子
クロスドキュメント メッセージング API (クロスドキュメント メッセージング)
フラグメント識別子
フラグメント識別子は、#
URL 番号の後の部分を指します (例http://example.com/x.html#fragment
: ) #fragment
。フラグメント識別子を変更しただけでは、ページは更新されません。
親ウィンドウは、子ウィンドウのフラグメント識別子に情報を書き込むことができます。
var src = オリジンURL + '#' + データ; document.getElementById('myIFrame').src = src;
上記のコードでは、親ウィンドウは送信する情報を iframe ウィンドウのフラグメント識別子に書き込みます。
子ウィンドウはhashchange
イベントをリッスンすることによって通知されます。
window.onhashchange = checkMessage; function checkMessage() { var message = window.location.hash; // ... }
同様に、子ウィンドウは親ウィンドウのフラグメント識別子を変更できます。
parent.location.href = ターゲット + '#' + ハッシュ;
window.postMessage()
上記の方法はクラックであり、この問題を解決するために、HTML5 ではクロスドキュメント メッセージング API (クロスドキュメント メッセージング) というまったく新しい API が導入されています。
この API は、2 つのウィンドウのオリジンが同じかどうかに関係なく、クロスウィンドウ通信を可能にするwindow
新しいメソッドをオブジェクトに追加します。window.postMessage
たとえば、親ウィンドウが子aaa.com
ウィンドウにメッセージを送信する場合はbbb.com
、postMessage
メソッドを呼び出すだけです。
// 親ウィンドウが子ウィンドウを開きます var Popup = window.open('http://bbb.com', 'title'); // 親ウィンドウが子ウィンドウにメッセージを送信します Popup.postMessage('Hello World !'、' http://bbb.com');
postMessage
メソッドの最初のパラメータは特定の情報の内容、2 番目のパラメータはメッセージを受信するウィンドウの起点、つまり「プロトコル + ドメイン名 + ポート」です。*
ドメイン名が制限されず、すべてのウィンドウに送信されるように設定することもできます。
子ウィンドウが親ウィンドウにメッセージを送信する方法は同様です。
// 子ウィンドウは親ウィンドウにメッセージを送信します window.opener.postMessage('Nice to see you', 'http://aaa.com');
親ウィンドウと子ウィンドウはどちらも、message
イベントを通じて互いのメッセージをリッスンできます。
// 親ウィンドウと子ウィンドウの両方で次のコードを使用できます。 // メッセージ メッセージをリッスンする window.addEventListener('message', function (e) { console.log(e.data); },false);
message
イベントのパラメータはイベント オブジェクトでありevent
、次の 3 つのプロパティを提供します。
event.source
:メッセージ送信画面
event.origin
: メッセージの送信先 URL
event.data
:メッセージ内容
次の例では、子ウィンドウがevent.source
プロパティを通じて親ウィンドウを参照し、メッセージを送信します。
window.addEventListener('メッセージ', acceptMessage); function acceptMessage(event) { event.source.postMessage('初めまして!', '*'); }
上記のコードについては注意すべき点がいくつかあります。まず、receiveMessage
関数内にフィルタリングされた情報のソースはなく、任意の URL から送信された情報が処理されます。次に、postMessage
メソッドで指定されたターゲット ウィンドウの URL はアスタリスクであり、情報を任意の URL に送信できることを示します。一般に、これら 2 つの方法は安全性が十分ではなく、悪意を持って悪用される可能性があるため、推奨されません。
event.origin
プロパティでは、このウィンドウに送信されないメッセージをフィルタリングできます。
window.addEventListener('メッセージ', acceptMessage); function acceptMessage(event) { if (event.origin !== 'http://aaa.com') return; if (event.data === 'Hello World') { event.source.postMessage('Hello',event.origin); } else { console.log(event.data); } }
ローカルストレージ
これによりwindow.postMessage
、他のウィンドウの LocalStorage の読み書きも可能になります。
以下は、メイン ウィンドウが iframe の子ウィンドウに書き込まれる例ですlocalStorage
。
window.onmessage = function(e) { if (e.origin !== 'http://bbb.com') { return; var ペイロード = JSON.parse(e.data); localStorage.setItem(payload.key, JSON.stringify(payload.data)); };
上記のコードでは、子ウィンドウは親ウィンドウから送信されたメッセージを独自の LocalStorage に書き込みます。
親ウィンドウからメッセージを送信するコードは次のとおりです。
var win = document.getElementsByTagName('iframe')[0].contentWindow; var obj = { 名前: 'ジャック' }; win.postMessage( JSON.stringify({key: 'storage', data: obj}), 'http://bbb.com' );
メッセージを受信するための子ウィンドウの拡張版のコードは次のとおりです。
window.onmessage = function(e) { if (e.origin !== 'http://bbb.com') return; var ペイロード = JSON.parse(e.data); switch (payload.method) { case 'set': localStorage.setItem(payload.key, JSON.stringify(payload.data)); 壊す; case 'get': varparent = window.parent; var data = localStorage.getItem(payload.key); parent.postMessage(data, 'http://aaa.com'); 壊す; ケース '削除': localStorage.removeItem(payload.key); 壊す; } };
親ウィンドウのメッセージ送信コードの拡張版は以下のとおりです。
var win = document.getElementsByTagName('iframe')[0].contentWindow; var obj = { 名前: 'ジャック' }; // 存在对象 win.postMessage( JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://bbb.com' ); // 读取对象 win.postMessage( JSON.stringify({key: 'storage', method: "get"}), "*" ); window.onmessage = function(e) { if (e.origin != 'http://aaa.com') return; console.log(JSON.parse(e.data).name); };
アヤックス
同一生成元ポリシーでは、AJAX リクエストは同じ生成元の URL にのみ送信できると規定されており、送信できない場合はエラーが報告されます。
サーバー プロキシを設定する (ブラウザーが同じオリジン サーバーを要求し、そのサーバーが外部サービスを要求する) 以外にも、この制限を回避するには 3 つの方法があります。
JSONP
ウェブソケット
コルス
JSONP
JSONP は、サーバーとクライアント間のクロスオリジン通信の一般的な方法です。最大の特徴は、シンプルで使いやすく、互換性の問題がなく、すべての古いブラウザがサポートし、サーバー側の変更が非常に少ないことです。
作り方は次のとおりです。
最初のステップでは、Web ページは<script>
要素を追加し、サーバーからスクリプトを要求します。これは同一生成元ポリシーによる制限を受けず、ドメイン間で要求できます。
<script src="http://api.foo.com?callback=bar"></script>
要求されたスクリプト URL にはcallback
パラメータ ( ) があり、これはクライアントのコールバック関数 ( )?callback=bar
の名前をサーバーに伝えるために使用されることに注意してください。bar
2 番目のステップでは、リクエストを受信した後、サーバーは文字列を連結し、JSON データを関数名に入れて文字列 ( bar({...})
) として返します。
3 番目のステップでは、クライアントはサーバーから返された文字列をコードとして解析します。ブラウザは、これが<script>
タグによって要求されたスクリプト コンテンツであると認識するためです。bar()
このとき、クライアントは関数を定義していれば、サーバーから返されるJSONデータを関数本体で取得できます。
以下に例を見てみましょう。まず、Web ページは、<script>
クロスドメイン URL にリクエストを行う要素を動的に挿入します。
function addScriptTag(src) { var script = document.createElement('script'); script.setAttribute('type', 'text/javascript'); スクリプト.src = ソース; document.body.appendChild(スクリプト); } foo({}) window.onload = function () { addScriptTag('http://example.com/ip?callback=foo'); } function foo(data) { console.log('あなたのパブリック IP アドレスは: ' + data.ip); };
<script>
上記のコードは、要素を動的に追加することによってサーバーにリクエストを送信しますexample.com
。このリクエストのクエリ文字列には、callback
JSONP に必要なコールバック関数の名前を指定するパラメータがあることに注意してください。
サーバーはこのリクエストを受信すると、コールバック関数のパラメータ位置にデータを入れて返します。
foo({ 'ip': '8.8.8.8' });
<script>
要素によって要求されたスクリプトはコードとして直接実行されるため。foo
このとき、ブラウザが関数を定義していれば、すぐに関数が呼び出されます。パラメータとしての JSON データは文字列ではなく JavaScript オブジェクトとして扱われるため、使用JSON.parse
手順が回避されます。
ウェブソケット
WebSocket は、プロトコル プレフィックスとしてws://
(非暗号化) と(暗号化)を使用する通信プロトコルです。wss://
このプロトコルは同一オリジン ポリシーを実装しておらず、サーバーがサポートしている限り、クロスオリジン通信を介して実行できます。
以下は、ブラウザが発行する WebSocket リクエストのヘッダー情報の例です ( Wikipediaから引用)。
GET /chat HTTP/1.1 ホスト:server.example.com アップグレード:websocket 接続:アップグレード Sec-WebSocket-Key:x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol:チャット、スーパーチャット Sec-WebSocket-Version:13 Origin:http://例.com
Origin
上記のコードには、リクエストのリクエストソース (オリジン)、つまりリクエストの送信元のドメイン名を示すフィールドがあります。
Origin
WebSocket が同一生成元ポリシーを実装しないのは、まさにこのフィールドのためです。サーバーはこのフィールドに基づいてこの通信を許可するかどうかを決定できるためです。ドメイン名がホワイトリストに含まれている場合、サーバーは次のように応答します。
HTTP/1.1 101 スイッチング プロトコル アップグレード: websocket 接続: アップグレード Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: チャット
コルス
CORS は Cross-Origin Resource Sharing の略称です。これは W3C 標準であり、クロスオリジン AJAX リクエストの基本的なソリューションです。リクエストの送信のみが可能な JSONP と比較してGET
、CORS ではあらゆる種類のリクエストが許可されます。
次の章では、CORS を通じてクロスオリジン AJAX リクエストを完了する方法を詳しく紹介します。
参考リンク
-
Mozilla Developer Network、Window.postMessage
-
Jakub Jankiewicz、クロスドメイン LocalStorage
-
David Baron、遅延を短くして setTimeout : window.postMessage を使用してコールバック関数を 0 ミリ秒でトリガーします