「同源」(同網站) 政策限制視窗和框架彼此存取。
假設使用者開啟兩個網頁:一個來自 john-smith.com
,另一個來自 gmail.com
,那麼他們不會希望來自 john-smith.com
的腳本讀取我們來自 gmail.com
的郵件。因此,「同源」政策的目的是保護使用者免於資訊被竊取。
同源
如果兩個 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
:我們可以變更它(因此重新導向使用者)。但是我們無法讀取位置(所以我們無法得知使用者現在在哪裡,沒有資訊外洩)。
實際應用:iframe
<iframe>
標籤會主控一個獨立的嵌入式視窗,它有自己的獨立 document
和 window
物件。
我們可以使用屬性存取它們
iframe.contentWindow
可取得<iframe>
內的視窗。iframe.contentDocument
可取得<iframe>
內的文件,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
與 iframe.contentWindow.onload
iframe.onload
事件(在 <iframe>
標籤上)基本上與 iframe.contentWindow.onload
(在嵌入式視窗物件上)相同。當嵌入式視窗完全載入所有資源時,它會觸發。
…但是我們無法存取來自另一個來源的 iframe 的 iframe.contentWindow.onload
,因此使用 iframe.onload
。
子網域中的 Windows:document.domain
根據定義,具有不同網域的兩個 URL 具有不同的來源。
但是,如果 Windows 共享相同的二級網域,例如 john.site.com
、peter.site.com
和 site.com
(因此它們的共同二級網域是 site.com
),我們可以讓瀏覽器忽略該差異,以便它們可以被視為來自「相同來源」以進行跨視窗通訊。
為了讓它運作,每個這樣的視窗都應該執行程式碼
document.domain = 'site.com';
這樣就夠了。現在它們可以毫無限制地進行互動。再次強調,這僅適用於具有相同二級網域的頁面。
document.domain
屬性正在從 規格 中移除。跨視窗訊息傳遞(稍後說明)是建議的替代方案。
話雖如此,截至目前為止,所有瀏覽器都支援它。而且支援將會保留在未來,以免中斷依賴 document.domain
的舊程式碼。
Iframe:錯誤的 document 陷阱
當 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.frames
中取得。
- 依據數字:
window.frames[0]
- 文件中第一個框架的視窗物件。 - 依據名稱:
window.frames.iframeName
- 具有name="iframeName"
的框架的視窗物件。
例如
<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 使用「不同來源」政策。換句話說,它會讓瀏覽器將iframe
視為來自其他來源,即使其src
指向同一個網站。這會套用所有對指令碼的隱含限制。此選項會移除該功能。 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 函式。這讓使用者更安全。
介面有兩個部分。
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.dev.org.tw
。 source
- 指向傳送者視窗的參考。如果我們想要,可以立即
source.postMessage(...)
回傳。
若要指定該處理常式,我們應該使用 addEventListener
,簡短語法 window.onmessage
無法使用。
以下是一個範例
window.addEventListener("message", function(event) {
if (event.origin != 'https://javascriptinfo.dev.org.tw') {
// 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
介面允許具有任何來源的兩個視窗進行對話
-
傳送者呼叫
targetWin.postMessage(data, targetOrigin)
。 -
如果
targetOrigin
不是'*'
,則瀏覽器會檢查視窗targetWin
是否具有來源targetOrigin
。 -
如果是,則
targetWin
會觸發message
事件,並具有特殊屬性origin
– 傳送者視窗的來源(例如http://my.site.com
)source
– 對傳送者視窗的參考。data
– 資料,在除了僅支援字串的 IE 之外的所有環境中為任何物件。
我們應該使用
addEventListener
在目標視窗內設定此事件的處理常式。
留言
<code>
標籤,要插入多行程式碼 - 請將它們包在<pre>
標籤中,要插入超過 10 行的程式碼 - 請使用 sandbox(plnkr、jsbin、codepen…)