#オリジナル制限

ブラウザのセキュリティの基礎は、「同一オリジン ポリシー」 (同一オリジン ポリシー) です。多くの開発者はこのことを知っていますが、完全には理解していません。

概要

意味

1995 年に、同一生成元ポリシーが Netscape によってブラウザに導入されました。現在、すべてのブラウザがこのポリシーを実装しています。

当初の意味は、2 つの Web ページのオリジンが同じでない限り、Web ページ A によって設定された Cookie を Web ページ B で開くことはできないということです。いわゆる「同源」とは「3つの類似点」を指します。

  • 同じプロトコル
  • 同じドメイン名
  • ポートは同じです (これは無視できます。詳細については以下を参照してください)

たとえば、URL http://www.example.com/dir/page.html の場合、プロトコルは 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 リクエストを元のアドレス以外に送信することはできません(送信はできますが、ブラウザは応答の受け入れを拒否します)。

さらに、他のウィンドウの「window」オブジェクトは、JavaScript スクリプトを通じて取得できます。オリジナルではない Web ページの場合、ウィンドウは現在、他の Web ページの「window」オブジェクトの 9 つのプロパティと 4 つのメソッドにアクセスできます。

  • 窓が閉まっている
  • ウィンドウフレーム -window.length
  • 窓の場所
  • ウィンドウオープナー
  • window.parent
  • window.self
  • window.top
  • 窓.窓
  • window.blur()
  • window.close()
  • window.focus()
  • window.postMessage()

上記の 9 つのプロパティのうち、「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 にリセットされるため、1 つの Web ページに対してのみ 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 を設定するときに、「example.com」などの Cookie のドメイン名を第 1 レベルのドメイン名として指定することもできます。

セット-Cookie: キー=値=example.com;

この場合、第 2 レベル ドメイン名と第 3 レベル ドメイン名の両方が設定なしでこの Cookie を読み取ることができます。

iframe とマルチウィンドウ通信

iframe 要素は、現在の Web ページ内の他の Web ページに埋め込むことができます。各 iframe 要素は独自のウィンドウを形成します。つまり、独自の window オブジェクトを持ちます。 iframe ウィンドウ内のスクリプトは親ウィンドウと子ウィンドウを取得できます。ただし、親ウィンドウと子ウィンドウは同じオリジンを持つ場合にのみ通信でき、ドメインを越えた場合は相手の DOM を取得できません。

たとえば、親ウィンドウが次のコマンドを実行する場合、「iframe」ウィンドウが同じソースからのものでない場合、エラーが報告されます。

書類
.getElementById("myIFrame")
.contentWindow
。書類
// Uncaught DOMException: フレームがクロスオリジン フレームにアクセスすることをブロックしました。

上記のコマンドでは、親ウィンドウは子ウィンドウの DOM を取得しようとしていますが、クロスドメインのためエラーが報告されます。

逆に、子ウィンドウもメイン ウィンドウの DOM を取得するときにエラーを報告します。

window.parent.document.body
// エラーを報告する

この状況は、iframe ウィンドウだけでなく、window.open メソッドによって開かれたウィンドウにも当てはまります。ドメインを越える限り、親ウィンドウと子ウィンドウは通信できません。

2 つのウィンドウの第 1 レベルのドメイン名が同じで、第 2 レベルのドメイン名だけが異なる場合、前のセクションで紹介した document.domain 属性を設定すると、同一生成元ポリシーを回避して DOM を取得できます。 。

まったく異なる起源を持つ Web サイトの場合、クロスドメイン ウィンドウの通信の問題を解決するには、現在 2 つの方法があります。

  • フラグメント識別子
  • クロスドキュメントメッセージング API (クロスドキュメントメッセージング)

フラグメント識別子

フラグメント識別子は、「http://example.com/x.html#fragment」の「#fragment」など、URL の「#」記号以降の部分を指します。フラグメント識別子を変更しただけでは、ページは更新されません。

親ウィンドウは、子ウィンドウのフラグメント識別子に情報を書き込むことができます。

var src = オリジンURL + '#' + データ;
document.getElementById('myIFrame').src = src;

上記のコードでは、親ウィンドウは送信する情報を iframe ウィンドウのフラグメント識別子に書き込みます。

子ウィンドウは、「hashchange」イベントをリッスンすることによって通知されます。

window.onhashchange = checkMessage;

関数 checkMessage() {
  var メッセージ = window.location.hash;
  // ...
}

同様に、子ウィンドウは親ウィンドウのフラグメント識別子を変更できます。

parent.location.href = ターゲット + '#' + ハッシュ;

window.postMessage()

上記の方法はこの問題を解決するために、クロスドキュメント メッセージング API (クロスドキュメント メッセージング) というまったく新しい API を導入しています。

この API は、新しい window.postMessage メソッドを window オブジェクトに追加し、2 つのウィンドウのオリジンが同じかどうかに関係なく、クロスウィンドウ通信を可能にします。たとえば、親ウィンドウ「aaa.com」が子ウィンドウ「bbb.com」にメッセージを送信する場合は、「postMessage」メソッドを呼び出すだけです。

//親ウィンドウが子ウィンドウを開きます
var ポップアップ = window.open('http://bbb.com', 'title');
//親ウィンドウは子ウィンドウにメッセージを送信します
Popup.postMessage('Hello World!', 'http://bbb.com');

postMessage メソッドの最初のパラメータは特定のメッセージの内容、2 番目のパラメータはメッセージを受信するウィンドウの起点、つまり「プロトコル + ドメイン名 + ポート」です。また、「*」に設定することもできます。これは、ドメイン名が制限されず、すべてのウィンドウに送信されることを意味します。

子ウィンドウが親ウィンドウにメッセージを送信する方法は同様です。

//子ウィンドウは親ウィンドウにメッセージを送信します
window.opener.postMessage('初めまして', 'http://aaa.com');

親ウィンドウと子ウィンドウは両方とも、「message」イベントを通じて互いのメッセージを聞くことができます。

//次のコードは親ウィンドウと子ウィンドウの両方に使用できます。
//メッセージメッセージを聞く
window.addEventListener('メッセージ', function (e) {
  console.log(e.data);
}、間違い);

message イベントのパラメータはイベント オブジェクト event であり、次の 3 つのプロパティを提供します。

  • event.source: メッセージを送信するウィンドウ
  • event.origin: メッセージ送信者の起点、つまりプロトコル、ドメイン名、ポート。
  • event.data: メッセージの内容

次の例では、子ウィンドウは event.source プロパティを通じて親ウィンドウを参照し、メッセージを送信します。

window.addEventListener('メッセージ', acceptMessage);
関数受信メッセージ(イベント) {
  event.source.postMessage('初めまして!', '*');
}

上記のコードについては注意すべき点がいくつかあります。まず、「receiveMessage」関数にはフィルタリングされた情報のソースはなく、任意の URL から送信された情報が処理されます。次に、postMessage メソッドで指定されたターゲット ウィンドウの URL はアスタリスクであり、メッセージを任意の URL に送信できることを示します。一般に、これら 2 つの方法は安全性が十分ではなく、悪意を持って悪用される可能性があるため、推奨されません。

「event.origin」プロパティは、許可されていないアドレスから送信されたメッセージをフィルタリングできます。

window.addEventListener('メッセージ', acceptMessage);
関数受信メッセージ(イベント) {
  if (event.origin !== 'http://aaa.com') return;
  if (event.data === 'Hello World') {
    event.source.postMessage('Hello',event.origin);
  } それ以外 {
    コンソール.ログ(イベント.データ);
  }
}

ローカルストレージ

window.postMessage を通じて、他のウィンドウの LocalStorage を読み書きすることも可能です。

以下は、メイン ウィンドウが iframe 子ウィンドウの localStorage に書き込む例です。

window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') {
    戻る;
  }
  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({キー: 'ストレージ', データ: obj}),
  「http://bbb.com」
);

メッセージを受信するための子ウィンドウの拡張版のコードは次のとおりです。

window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') return;
  var ペイロード = JSON.parse(e.data);
  スイッチ (ペイロード.メソッド) {
    ケース「セット」:
      localStorage.setItem(payload.key, JSON.stringify(payload.data));
      壊す;
    ケース「取得」:
      var 親 = 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({キー: 'ストレージ'、メソッド: 'セット'、データ: obj}),
  「http://bbb.com」
);
// オブジェクトを読み取ります
win.postMessage(
  JSON.stringify({キー: 'ストレージ', メソッド: "get"}),
  「*」
);
window.onmessage = function(e) {
  if (e.origin != 'http://aaa.com') return;
  console.log(JSON.parse(e.data).name);
};

アヤックス

同一オリジン ポリシーでは、AJAX リクエストは同じオリジンを持つ URL にのみ送信できると規定されており、送信できない場合はエラーが報告されます。

サーバー プロキシを設定する (ブラウザーが同じオリジン サーバーを要求し、そのサーバーが外部サービスを要求する) 以外にも、この制限を回避するには 3 つの方法があります。

-JSONP

  • Webソケット
  • CORS

###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 ページは、クロスドメイン URL にリクエストを行う <script> 要素を動的に挿入します。

関数 addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute('type', 'text/javascript');
  スクリプト.src = ソース;
  document.body.appendChild(スクリプト);
}

window.onload = 関数 () {
  addScriptTag('http://example.com/ip?callback=foo');
}

関数 foo(データ) {
  console.log('あなたのパブリック IP アドレスは: ' + data.ip);
};

上記のコードは、<script> 要素を動的に追加することで、サーバー example.com にリクエストを作成します。このリクエストのクエリ文字列には、JSONP に必要なコールバック関数の名前を指定するために使用される「callback」パラメータがあることに注意してください。

サーバーはこのリクエストを受信すると、コールバック関数のパラメータ位置にデータを入れて返します。

foo({
  'ip': '8.8.8.8'
});

<script> 要素によって要求されたスクリプトは、コードとして直接実行されます。このとき、ブラウザに foo 関数が定義されていれば、すぐにその関数が呼び出されます。パラメータとしての JSON データは文字列ではなく JavaScript オブジェクトとして扱われるため、「JSON.parse」を使用する手順が不要になります。

Webソケット

WebSocket は、プロトコル プレフィックスとして「ws://」(非暗号化)および「wss://」(暗号化)を使用する通信プロトコルです。このプロトコルは同一オリジン ポリシーを実装しておらず、サーバーがサポートしている限り、クロスオリジン通信を介して実行できます。

以下は、ブラウザが発行する WebSocket リクエストのヘッダー情報の例です (Wikipedia から引用)。

GET /チャット HTTP/1.1
ホスト:server.example.com
アップグレード: Webソケット
接続: アップグレード
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: チャット、スーパーチャット
Sec-WebSocket-バージョン: 13
出典: http://example.com

上記のコードには、「Origin」というフィールドがあり、リクエストのリクエスト元(オリジン)、つまりリクエストの送信元のドメイン名を示します。

WebSocket が同一オリジン ポリシーを実装しないのは、まさに「Origin」フィールドのためです。サーバーはこのフィールドに基づいてこの通信を許可するかどうかを決定できるためです。ドメイン名がホワイトリストに含まれている場合、サーバーは次のように応答します。

HTTP/1.1 101 スイッチングプロトコル
アップグレード: Webソケット
接続: アップグレード
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-プロトコル: チャット

CORS

CORS は Cross-Origin Resource Sharing の略称です。これは W3C 標準であり、クロスオリジン AJAX リクエストの基本的なソリューションです。 「GET」リクエストのみを送信できる JSONP と比較して、CORS ではあらゆるタイプのリクエストが許可されます。

次の章では、CORS を通じてクロスオリジン AJAX リクエストを完了する方法を詳しく紹介します。

参考リンク


作者: wangdoc

アドレス: https://wangdoc.com/

ライセンス: クリエイティブ・コモンズ 3.0