本篇要來介紹 iframe security,內容包含
<iframe sandbox>
<iframe sandbox>
根據 html.spec.whatwg.org 的文件描述
When the attribute is set, the content is treated as being from a unique opaque origin, forms, scripts, and various potentially annoying APIs are disabled, and links are prevented from targeting other navigables.
也就是說,當我們設定 <iframe sandbox src="URL"></iframe>
的時候,就會限制 <iframe>
內的網站行為,但我們可以透過設定多組 allow-
的 token 來指定被嵌入的頁面可以執行哪些事情,包含以下:
我們建立以下:
檔案名稱 | 用途 |
---|---|
5000.html | 主網站,等等要透過瀏覽器打開 |
5000sandbox.html | 被嵌入 <iframe> 的網站 |
5000sandbox-popup.html | 被嵌入 <iframe> 的網站所開啟的新分頁 |
index.ts | NodeJS HTTP Server |
download.js | 被下載的測試檔案,內容隨意 |
5000.html
<html>
<head></head>
<body>
<h1>5000.html</h1>
<style>
iframe {
width: 100%;
height: 300px;
}
</style>
<iframe
src="http://localhost:5000/sandbox"
sandbox="allow-scripts"
></iframe>
</body>
</html>
5000sandbox.html
<html>
<head></head>
<body>
<h1>5000sandbox.html</h1>
<h3>allow-downloads</h3>
<div>
<a download href="http://localhost:5000/download">
download file with download attribute
</a>
<br />
<a href="http://localhost:5000/download">
navigate to a URL with Content-Disposition: attachment
</a>
<br />
<script>
function handleDownload() {
const a = document.createElement("a");
a.href = "http://localhost:5000/download";
a.click();
}
</script>
<button onclick="handleDownload()">download file with js control</button>
</div>
</body>
</html>
index.ts
import { readFileSync } from "fs";
import { http5000Server } from "./httpServers";
import { join } from "path";
import { faviconListener } from "../listeners/faviconListener";
import { notFoundListener } from "../listeners/notFoundlistener";
// 為了開發方便,每次 request 都去讀取 static html
http5000Server.removeAllListeners("request");
http5000Server.on("request", function requestListener(req, res) {
if (req.url === "/favicon.ico") return faviconListener(req, res);
if (req.url === "/") {
res.setHeader("Content-Type", "text/html; charset=utf-8");
return res.end(readFileSync(join(__dirname, "5000.html")));
}
if (req.url === "/sandbox") {
res.setHeader("Content-Type", "text/html; charset=utf-8");
return res.end(readFileSync(join(__dirname, "5000sandbox.html")));
}
if (req.url === "/download") {
res.setHeader("Content-Type", "text/javascript");
res.setHeader("Content-Disposition", "attachment; filename=download.js");
return res.end(readFileSync(join(__dirname, "download.js")));
}
return notFoundListener(req, res);
});
download.js
console.log("downloaded js file!!!");
瀏覽器打開 http://localhost:5000/ ,點擊下載連結跟按鈕,會看到以下錯誤訊息
把 allow-downloads
加上去
5000.html
<iframe
src="http://localhost:5000/sandbox"
sandbox="allow-scripts allow-downloads"
></iframe>
重整畫面,點擊下載按鈕,此時就可以正常下載了 ✨✨✨
5000.html
<iframe src="http://localhost:5000/sandbox" sandbox=""></iframe>
5000sandbox.html
<h3>allow-forms</h3>
<form
method="post"
action="http://localhost:5000/form"
enctype="multipart/form-data"
>
<input type="text" placeholder="請輸入帳號" name="username" />
<button type="submit">送出</button>
</form>
<dialog open>
<form method="dialog">
<input type="text" placeholder="請輸入帳號" name="username" pattern="" />
<button type="submit">Close Dialog</button>
</form>
</dialog>
<form
method="post"
action="http://localhost:5000/form"
enctype="multipart/form-data"
>
<input
required
type="text"
placeholder="請輸入帳號(長度4~16)"
name="username"
pattern="\w{4,16}"
/>
<button type="submit">驗證 && 送出</button>
</form>
index.ts
if (req.url === "/form") {
res.setHeader("Content-Type", "text/plain");
return res.end("form submitted");
}
瀏覽器打開 http://localhost:5000/ ,點擊按鈕,會看到以下錯誤訊息
把 allow-forms
加上去
5000.html
<iframe src="http://localhost:5000/sandbox" sandbox="allow-forms"></iframe>
重整畫面,點擊 submit 按鈕,此時就可以正常表單驗證 & 送出表單了 ✨✨✨
5000.html
<iframe src="http://localhost:5000/sandbox" sandbox="allow-scripts"></iframe>
5000sandbox.html
<h3>allow-modals</h3>
<script>
function promptUsername() {
const username = prompt("username");
console.log(username);
}
addEventListener("beforeunload", (e) => {
e.preventDefault();
e.returnValue = "beforeunload";
return "beforeunload";
});
</script>
<button onclick="alert('alert')">alert</button>
<button onclick="confirm('confirm')">confirm</button>
<button onclick="print()">print</button>
<button onclick="promptUsername()">prompt</button>
瀏覽器打開 http://localhost:5000/ ,點擊按鈕,會看到以下錯誤訊息
把 allow-modals
加上去
5000.html
<iframe
src="http://localhost:5000/sandbox"
sandbox="allow-scripts allow-modals"
></iframe>
重整畫面,點擊按鈕,此時就可以正常跳出 modal 了 ✨✨✨
5000.html
<iframe src="http://localhost:5000/sandbox" sandbox="allow-scripts"></iframe>
5000sandbox.html
<h3>allow-popups</h3>
<div>
<script>
function openExampleCom() {
open("https://example.com/", "_blank");
}
</script>
<a target="_blank" href="https://example.com/">open example.com</a>
<button onclick="openExampleCom()">open example.com</button>
</div>
瀏覽器打開 http://localhost:5000/ ,點擊按鈕,會看到以下錯誤訊息
把 allow-popups
加上去
5000.html
<iframe
src="http://localhost:5000/sandbox"
sandbox="allow-scripts allow-popups"
></iframe>
重整畫面,點擊按鈕,此時就可以正常開啟新視窗了 ✨✨✨
5000.html
<iframe src="http://localhost:5000/sandbox" sandbox="allow-popups"></iframe>
5000sandbox.html
<h3>allow-popups-to-escape-sandbox</h3>
<a target="_blank" href="http://localhost:5000/sandbox-popup">另開新頁</a>
<a target="_self" href="http://localhost:5000/sandbox-popup">原頁導轉</a>
5000sandbox-popup.html
<html>
<head></head>
<body>
<h1>5000sandbox-popup.html</h1>
<h2 id="h2" style="display: none">
JavaScript is enabled (allow-popups-to-escape-sandbox)
</h2>
<script>
document.getElementById("h2").style.display = "block";
</script>
<noscript>
<h2>JavaScript is disabled (not-allow-popups-to-escape-sandbox)</h2>
</noscript>
</body>
</html>
index.ts
if (req.url === "/sandbox-popup") {
res.setHeader("Content-Type", "text/html; charset=utf-8");
return res.end(readFileSync(join(__dirname, "5000sandbox-popup.html")));
}
瀏覽器打開 http://localhost:5000/ ,分別點擊兩個按鈕
原頁導轉
另開新頁
把 allow-popups-to-escape-sandbox
加上去
5000.html
<iframe
src="http://localhost:5000/sandbox"
sandbox="allow-popups allow-popups-to-escape-sandbox"
></iframe>
重整畫面,分別點擊兩個按鈕
原頁導轉
另開新頁
allow-popups-to-escape-sandbox
,所以可以跳出 sandbox 的限制(可以執行 script)5000.html
<iframe src="http://localhost:5000/sandbox" sandbox="allow-scripts"></iframe>
5000sandbox.html
<h3>allow-top-navigation-by-user-activation</h3>
<script>
function navigateTopToExampleCom() {
top.location.href = "https://example.com/";
}
</script>
<button onclick="navigateTopToExampleCom()">
top navigation to example.com
</button>
瀏覽器打開 http://localhost:5000/ ,點擊按鈕,會看到以下錯誤訊息
把 allow-top-navigation
加上去
5000.html
<iframe
src="http://localhost:5000/sandbox"
sandbox="allow-scripts allow-top-navigation"
></iframe>
重整畫面,點擊按鈕,此時就可以正常把 top window 導轉了 ✨✨✨
<iframe>
, <frame>
, <embed>
跟 <object>
嵌入<frame>
, <embed>
跟 <object>
都是比較老舊的 HTMLElement,故本篇會著重在 <iframe>
不讓任何網頁嵌入
只讓同源的網頁嵌入
比 X-Frame-Options
更新的 HTTP Response Header,可以提供更精細的控制,可設定多個白名單
跟 X-Frame-Options: DENY
類似,不讓任何網頁嵌入
跟 X-Frame-Options: SAMEORIGIN
類似,只讓同源的網頁嵌入
window
window.frames[number]
或 HTMLIFrameElement.contentWindow
存取嵌入的網站window.parent
存取 parent 網站window.postMessage
以及 addEventListener('message', callback)
window
底下的屬性,則會被瀏覽器擋下來Uncaught SecurityError: Failed to read a named property 'document' from 'Window': Blocked a frame with origin "http://localhost:5001" from accessing a cross-origin frame.
如果同時設定這兩個,瀏覽器會以哪個為更高優先度呢?我們使用 NodeJS HTTP 模組來試試看:
NodeJS
http5000Server.on("request", function requestListener(req, res) {
if (req.url === "/") {
res.setHeader("Content-Type", "text/html; charset=utf-8");
return res.end(readFileSync(join(__dirname, "5000.html")));
}
});
http5001Server.on("request", function requestListener(req, res) {
if (req.url === "/DENY+self") {
res.setHeader("X-Frame-Options", "DENY");
res.setHeader("Content-Security-Policy", "frame-ancestors 'self'");
}
});
5000.html
<html>
<head></head>
<body>
<h1>5000</h1>
<div>5000DENY+self</div>
<iframe src="http://localhost:5000/DENY+self"></iframe>
</body>
</html>
5000DENY+self.html
<html>
<head></head>
<body>
<h1>5000DENY+self</h1>
<script>
console.log("5000DENY+self.html", window.parent.document);
</script>
</body>
</html>
兩者都有設定的情況,CSP 的優先權會高於 X-Frame-Options
在 CSP2 的官方文件中有描述到這點
The frame-ancestors directive obsoletes the X-Frame-Options header. If a resource has both policies, the frame-ancestors policy SHOULD be enforced and the X-Frame-Options policy SHOULD be ignored.
因為 CSP frame-ancestors
是比較新的功能,為了瀏覽器的向後兼容性,建議兩者都設置
如果不想讓所有網站嵌入,就設定
res.setHeader("X-Frame-Options", "DENY");
res.setHeader("Content-Security-Policy", "frame-ancestors 'none'");
如果只想讓同源的網站嵌入,就設定
res.setHeader("X-Frame-Options", "SAMEORIGIN");
res.setHeader("Content-Security-Policy", "frame-ancestors 'self'");
如果想要更精細的控制哪些網站可嵌入,就設定
res.setHeader(
"Content-Security-Policy",
"frame-ancestors <host-source> <host-source>",
);