「同一オリジン」(同一サイト)ポリシーは、ウィンドウとフレームがお互いにアクセスすることを制限します。
その考え方は、ユーザーが2つのページを開いている場合、1つはjohn-smith.com
、もう1つはgmail.com
の場合、john-smith.com
のスクリプトがgmail.com
からメールを読み取ることは望ましくないということです。したがって、「同一オリジン」ポリシーの目的は、ユーザーを情報窃盗から保護することです。
同一オリジン
2つのURLは、プロトコル、ドメイン、ポートが同じ場合に「同一オリジン」であると言われます。
これらのURLはすべて同じオリジンを共有しています
http://site.com
http://site.com/
http://site.com/my/page.html
これらはそうではありません
http://www.site.com
(別のドメイン:www.
が重要です)http://site.org
(別のドメイン:.org
が重要です)https://site.com
(別のプロトコル:https
)http://site.com:8080
(別のポート:8080
)
「同一オリジン」ポリシーは、以下のことを述べています
- 別のウィンドウへの参照、たとえば
window.open
によって作成されたポップアップや<iframe>
内のウィンドウがあり、そのウィンドウが同じオリジンから来た場合、そのウィンドウへのフルアクセス権があります。 - そうでない場合、別のオリジンから来た場合は、そのウィンドウのコンテンツ(変数、ドキュメント、その他すべて)にアクセスできません。唯一の例外は
location
です。変更することができます(これによりユーザーをリダイレクトします)。しかし、locationを読み取ることはできません(そのため、ユーザーがどこにいるかを確認することはできません。情報漏えいはありません)。
動作: iframe
<iframe>
タグは、独自の分離されたdocument
およびwindow
オブジェクトを持つ、別の埋め込みウィンドウをホストします。
プロパティを使用してそれらにアクセスできます
<iframe>
内のウィンドウを取得するにはiframe.contentWindow
を使用します。<iframe>
内のドキュメントを取得するにはiframe.contentDocument
を使用します。これはiframe.contentWindow.document
の省略形です。
埋め込みウィンドウ内のものにアクセスするとき、ブラウザはiframeが同じオリジンを持っているかどうかを確認します。そうでない場合、アクセスは拒否されます(location
への書き込みは例外で、引き続き許可されます)。
たとえば、別のオリジンから<iframe>
の読み取りと書き込みを試してみましょう
<iframe src="https://example.com" id="iframe"></iframe>
<script>
iframe.onload = function() {
// we can get the reference to the inner window
let iframeWindow = iframe.contentWindow; // OK
try {
// ...but not to the document inside it
let doc = iframe.contentDocument; // ERROR
} catch(e) {
alert(e); // Security Error (another origin)
}
// also we can't READ the URL of the page in iframe
try {
// Can't read URL from the Location object
let href = iframe.contentWindow.location.href; // ERROR
} catch(e) {
alert(e); // Security Error
}
// ...we can WRITE into location (and thus load something else into the iframe)!
iframe.contentWindow.location = '/'; // OK
iframe.onload = null; // clear the handler, not to run it after the location change
};
</script>
上記のコードは、以下を除くすべての操作でエラーを示しています
- 内部ウィンドウへの参照を取得する
iframe.contentWindow
- これは許可されています。 location
への書き込み。
それとは対照的に、<iframe>
が同じオリジンを持っている場合、それに対して何でもできます
<!-- iframe from the same site -->
<iframe src="/" id="iframe"></iframe>
<script>
iframe.onload = function() {
// just do anything
iframe.contentDocument.body.prepend("Hello, world!");
};
</script>
iframe.onload
vs iframe.contentWindow.onload
iframe.onload
イベント(<iframe>
タグ上)は、基本的にはiframe.contentWindow.onload
(埋め込みウィンドウオブジェクト上)と同じです。埋め込みウィンドウがすべてのリソースとともに完全にロードされるとトリガーされます。
…しかし、別のオリジンからのiframeの場合、iframe.contentWindow.onload
にアクセスできないため、iframe.onload
を使用します。
サブドメイン上のウィンドウ: document.domain
定義上、異なるドメインを持つ2つのURLは異なるオリジンを持ちます。
しかし、ウィンドウが同じ第2レベルドメインを共有している場合、たとえばjohn.site.com
、peter.site.com
、site.com
(共通の第2レベルドメインがsite.com
になるように)、ブラウザにその違いを無視させることができます。したがって、ウィンドウ間の通信の目的のために、「同じオリジン」から来たものとして扱われるようにできます。
機能させるには、そのような各ウィンドウがコードを実行する必要があります
document.domain = 'site.com';
それだけです。これで、制限なしに相互作用できます。繰り返しますが、これは同じ第2レベルドメインを持つページでのみ可能です。
document.domain
プロパティは、仕様から削除される過程にあります。クロスウィンドウメッセージング(以下で説明)が推奨される代替手段です。
とはいえ、現在、すべてのブラウザがそれをサポートしています。また、document.domain
に依存する古いコードを壊さないように、将来もサポートは維持されます。
Iframe: 間違ったドキュメントの落とし穴
iframeが同じオリジンから来て、そのdocument
にアクセスできる場合、落とし穴があります。クロスオリジンのこととは関係ありませんが、知っておくことが重要です。
作成時、iframeにはすぐにドキュメントがあります。しかし、そのドキュメントは、ロードされるものとは異なります!
そのため、すぐにドキュメントで何かを行うと、おそらく失われます。
ここに見てください
<iframe src="/" id="iframe"></iframe>
<script>
let oldDoc = iframe.contentDocument;
iframe.onload = function() {
let newDoc = iframe.contentDocument;
// the loaded document is not the same as initial!
alert(oldDoc == newDoc); // false
};
</script>
まだロードされていないiframeのドキュメントを操作するべきではありません。それは間違ったドキュメントだからです。そこにイベントハンドラーを設定しても、無視されます。
ドキュメントが存在する瞬間を検出する方法は?
iframe.onload
がトリガーされたときに、正しいドキュメントが確実に配置されます。ただし、すべてのリソースを含むiframe全体がロードされた場合にのみトリガーされます。
setInterval
でのチェックを使用して、より早く瞬間をキャッチしようとすることができます
<iframe src="/" id="iframe"></iframe>
<script>
let oldDoc = iframe.contentDocument;
// every 100 ms check if the document is the new one
let timer = setInterval(() => {
let newDoc = iframe.contentDocument;
if (newDoc == oldDoc) return;
alert("New document is here!");
clearInterval(timer); // cancel setInterval, don't need it any more
}, 100);
</script>
コレクション: window.frames
<iframe>
のwindowオブジェクトを取得する別の方法は、名前付きコレクションwindow.frames
から取得することです
- 数値で:
window.frames[0]
- ドキュメント内の最初のフレームのwindowオブジェクト。 - 名前で:
window.frames.iframeName
-name="iframeName"
を持つフレームのwindowオブジェクト。
たとえば
<iframe src="/" style="height:80px" name="win" id="iframe"></iframe>
<script>
alert(iframe.contentWindow == frames[0]); // true
alert(iframe.contentWindow == frames.win); // true
</script>
iframeには内部に他のiframeがある場合があります。対応するwindow
オブジェクトは階層を形成します。
ナビゲーションリンクは次のとおりです
window.frames
- 「子」ウィンドウのコレクション(ネストされたフレームの場合)。window.parent
- 「親」(外側)ウィンドウへの参照。window.top
- 最上位の親ウィンドウへの参照。
たとえば
window.frames[0].parent === window; // true
top
プロパティを使用して、現在のドキュメントがフレーム内で開いているかどうかを確認できます
if (window == top) { // current window == window.top?
alert('The script is in the topmost window, not in a frame');
} else {
alert('The script runs in a frame!');
}
「サンドボックス」iframe属性
sandbox
属性を使用すると、信頼できないコードの実行を防ぐために、<iframe>
内での特定のアクションを除外できます。別のオリジンから来たものとして、または他の制限を適用することにより、iframeを「サンドボックス化」します。
<iframe sandbox src="...">
に適用される制限の「デフォルトセット」があります。ただし、<iframe sandbox="allow-forms allow-popups">
のように、属性の値として適用されない制限のスペース区切りのリストを提供すると、緩和できます。
言い換えれば、空の"sandbox"
属性は可能な限り最も厳しい制限を適用しますが、リフトしたいもののスペース区切りリストを配置できます。
制限のリストを次に示します
allow-same-origin
- デフォルトでは、
"sandbox"
はiframeに対して「異なるオリジン」ポリシーを強制します。言い換えれば、src
が同じサイトを指している場合でも、ブラウザにiframe
を別のオリジンから来たものとして扱わせます。スクリプトに対するすべての暗黙の制限付きです。このオプションはその機能を削除します。 allow-top-navigation
iframe
がparent.location
を変更できるようにします。allow-forms
iframe
からフォームを送信できるようにします。allow-scripts
iframe
からスクリプトを実行できるようにします。allow-popups
iframe
からwindow.open
ポップアップを開くことができるようにします
詳細については、マニュアルを参照してください。
以下の例は、デフォルトの制限セットを持つサンドボックス化されたiframeを示しています: <iframe sandbox src="...">
。いくつかのJavaScriptとフォームがあります。
何も機能しないことに注意してください。したがって、デフォルトセットは非常に厳しいものです
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div>The iframe below has the <code>sandbox</code> attribute.</div>
<iframe sandbox src="sandboxed.html" style="height:60px;width:90%"></iframe>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<button onclick="alert(123)">Click to run a script (doesn't work)</button>
<form action="http://google.com">
<input type="text">
<input type="submit" value="Submit (doesn't work)">
</form>
</body>
</html>
"sandbox"
属性の目的は、制限を追加するだけです。削除することはできません。特に、iframeが別のオリジンから来た場合、同一オリジンの制限を緩和することはできません。
ウィンドウ間のメッセージング
postMessage
インターフェースを使用すると、ウィンドウはどのオリジンから来たかに関係なく、お互いに通信できます。
したがって、「同一オリジン」ポリシーを回避する方法です。john-smith.com
からのウィンドウがgmail.com
と通信し、情報を交換することを許可しますが、両方が同意し、対応するJavaScript関数を呼び出す場合に限ります。これにより、ユーザーにとって安全になります。
インターフェースには2つの部分があります。
postMessage
メッセージを送信するウィンドウは、受信側のウィンドウのpostMessageメソッドを呼び出します。言い換えれば、メッセージをwin
に送信したい場合、win.postMessage(data, targetOrigin)
を呼び出す必要があります。
引数
data
- 送信するデータ。「構造化シリアル化アルゴリズム」を使用してデータが複製されます。IEは文字列のみをサポートしているため、そのブラウザをサポートするには複雑なオブジェクトを
JSON.stringify
する必要があります。 targetOrigin
- ターゲットウィンドウのオリジンを指定し、指定されたオリジンからのウィンドウのみがメッセージを取得できるようにします。
targetOrigin
は安全対策です。ターゲットウィンドウが別のオリジンから来た場合、送信側ウィンドウでそのlocation
を読み取ることができないことを忘れないでください。したがって、どのサイトが目的のウィンドウで現在開いているかを確認できません。ユーザーが移動してしまう可能性があり、送信側ウィンドウはそれについて何も知りません。
targetOrigin
を指定することで、ウィンドウが正しいサイトに存在する場合にのみデータを受信するようにできます。データが機密情報の場合に重要です。
例えば、ここで win
は、オリジンが http://example.com
のドキュメントを持っている場合にのみメッセージを受信します。
<iframe src="http://example.com" name="example">
<script>
let win = window.frames.example;
win.postMessage("message", "http://example.com");
</script>
このチェックが不要な場合は、targetOrigin
を *
に設定できます。
<iframe src="http://example.com" name="example">
<script>
let win = window.frames.example;
win.postMessage("message", "*");
</script>
onmessage
メッセージを受信するには、ターゲットウィンドウは message
イベントのハンドラを持っている必要があります。これは postMessage
が呼び出された時(および targetOrigin
チェックが成功した時)にトリガーされます。
イベントオブジェクトには特別なプロパティがあります。
data
postMessage
からのデータ。origin
- 送信者のオリジン。例えば
https://javascriptinfo.dokyumento.jp
のようなものです。 source
- 送信者ウィンドウへの参照。必要であれば、すぐに
source.postMessage(...)
で折り返すことができます。
そのハンドラを割り当てるには、addEventListener
を使う必要があります。短縮構文の window.onmessage
は機能しません。
以下に例を示します。
window.addEventListener("message", function(event) {
if (event.origin != 'https://javascriptinfo.dokyumento.jp') {
// something from an unknown domain, let's ignore it
return;
}
alert( "received: " + event.data );
// can message back using event.source.postMessage(...)
});
完全な例
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
Receiving iframe.
<script>
window.addEventListener('message', function(event) {
alert(`Received ${event.data} from ${event.origin}`);
});
</script>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<form id="form">
<input type="text" placeholder="Enter message" name="message">
<input type="submit" value="Click to send">
</form>
<iframe src="iframe.html" id="iframe" style="display:block;height:60px"></iframe>
<script>
form.onsubmit = function() {
iframe.contentWindow.postMessage(this.message.value, '*');
return false;
};
</script>
</body>
</html>
まとめ
別のウィンドウのメソッドを呼び出してコンテンツにアクセスするには、まずそのウィンドウへの参照が必要です。
ポップアップの場合、次の参照があります。
- オープナーウィンドウから:
window.open
– 新しいウィンドウを開き、それへの参照を返します。 - ポップアップから:
window.opener
– ポップアップからのオープナーウィンドウへの参照です。
iframe の場合、親/子ウィンドウには次を使用してアクセスできます。
window.frames
– ネストされたウィンドウオブジェクトのコレクション。window.parent
、window.top
は、親および最上位のウィンドウへの参照です。iframe.contentWindow
は、<iframe>
タグ内のウィンドウです。
ウィンドウが同じオリジン(ホスト、ポート、プロトコル)を共有している場合、ウィンドウは互いに何でもやりたいことができます。
そうでない場合、可能なアクションは次のとおりです。
- 別のウィンドウの
location
を変更する(書き込み専用アクセス)。 - それにメッセージを投稿します。
例外は次のとおりです。
- 同じセカンドレベルドメインを共有するウィンドウ:
a.site.com
およびb.site.com
。その場合、両方でdocument.domain='site.com'
を設定すると、両方が「同じオリジン」状態になります。 - iframe に
sandbox
属性がある場合、属性値にallow-same-origin
が指定されていない限り、強制的に「異なるオリジン」状態になります。これは、同じサイトからの iframe で信頼できないコードを実行するために使用できます。
postMessage
インターフェースを使用すると、任意のオリジンを持つ 2 つのウィンドウが通信できます。
-
送信者は
targetWin.postMessage(data, targetOrigin)
を呼び出します。 -
targetOrigin
が'*'
でない場合、ブラウザはウィンドウtargetWin
がオリジンtargetOrigin
を持っているかどうかをチェックします。 -
その場合、
targetWin
は、特別なプロパティを持つmessage
イベントをトリガーします。origin
– 送信元ウィンドウのオリジン(http://my.site.com
など)source
– 送信元ウィンドウへの参照。data
– データ。IE を除くすべての場所の任意のオブジェクト(IE は文字列のみをサポート)。
ターゲットウィンドウ内でこのイベントのハンドラを設定するには、
addEventListener
を使用する必要があります。
コメント
<code>
タグを使用し、複数行の場合は<pre>
タグでラップし、10行を超える場合はサンドボックス ( plnkr, jsbin, codepen...) を使用してください。