前端單兵基本教練 - X-Frame-Options、CSP frame-ancestors 網站內嵌限制實測
0 |
基於安全考量,現代網站通常會加上 HTTP Header X-Frame-Options 或 Content-Scurity-Policy(CSP) 防止 Clickjacking (點擊劫持)。(不知道 Clickjacking 的同學可參考 淺談 IFrame 式 Clickjacking 攻擊與防護,看一下其中「帥哥一秒變豬頭」的攻擊示範)
防範網頁被惡意網站內嵌成 IFrame 有兩種做法,有點過時的 X-Frame-Options (如果要支援 IE,這是唯一選擇),以及現代瀏覽器主打的 CSP frame-ancestors。
應用時可分為以下三種情境:
- 禁止任何網站內嵌
X-Frame-Options: DENY 或 Content-Security-Policy: frame-ancestors 'none'; - 限定只能被同網站內嵌(Scheme、Host、Port 都要相同)
X-Frame-Options: SAMEORIGIN 或 Content-Security-Policy: frame-ancestors 'self'; - 只允許特定網站內嵌
X-Frame-Options: ALLOW-FROM uri 只能被特定網站內嵌,但這個規格只有 IE8+ 跟 Firefox for Android 支援,只限單一 Uri。
CSP 寫法的彈性很多,可以包含 'self'、列舉多個 Url、甚至支援萬用字元(可用於 Host 及 Port):
Content-Security-Policy: frame-ancestors 'self' https://www.example.org http://\*.another-web.ne http://www.all-port-ok.net:*
但是 CSP 到 IE11 都不支援。
今天打算做個完整實測驗證,確保自己理解正確。
設計的實驗如下,在 IIS 放一個能以 QueryString fo、fa 參數控制 X-Frame-Options 及 Content-Security-Policy Header 的 ASPX - child.aspx:
<%@Page Language="C#"%>
<script runat="server">
protected string headerInfo = string.Empty;
void Page_Load(object sender, EventArgs e)
{
string frameOptions = string.Empty;
var fo = Request["fo"];
switch (fo)
{
case "d": frameOptions = "DENY"; break;
case "s": frameOptions = "SAMEORIGIN"; break;
case "a": frameOptions = "ALLOW-FROM http://parent.utopia.net"; break;
}
if (!string.IsNullOrEmpty(fo))
{
Response.Headers.Add("X-Frame-Options", frameOptions);
headerInfo = "<div>X-Frame-Options: " + frameOptions + "</div>";
}
string frameAcestors = string.Empty;
var fa = Request["fa"];
switch (fa)
{
case "d": frameAcestors = "'none'"; break;
case "s": frameAcestors = "'self'"; break;
case "a": frameAcestors = "'self' http://parent.utopia.net"; break;
case "m": frameAcestors = "'self' http://parent.utopia.net http://grand-parent.utopia.net"; break;
}
if (!string.IsNullOrEmpty(frameAcestors))
{
Response.Headers.Add("Content-Security-Policy", "frame-ancestors " + frameAcestors);
headerInfo += "<div>Content-Security-Policy: frame-ancestors " + frameAcestors + "</div>";
}
}
</script>
<html>
<head>
<title>Grand Parent</title>
<style>html,body {font-size:9pt;}</style>
</head>
<body>
<%= headerInfo %>
</body>
</html>
為模擬多個網站來源,我在 C:\Windows\System32\drivers\etc\hosts 設定 child.utopia.net、parent.utopia.net、grand-parent.utopia 都指向 127.0.0.1。因此可用不同 Host 存取同一個實體 ASPX 檔,例如:ℎttp://child.utopia.net/aspnet/iframetest/child.aspx 與 ℎttp://parent.utopia.net/aspnet/iframetest/child.aspx 都連到本機 IIS 的同一網頁,但對瀏覽器為不同來源,藉此模擬不同網站來源搭配的情境,
在 parent.aspx 我放了七個 IFrame 以不同參數嵌入 child.aspx,分別是:
- X-Frame-Options: DENY
- X-Frame-Options: SAMEORIGIN
- X-Frame-Options: ALLOW-FROM ℎttp://parent.utopia.net
- Content-Security-Policy: frame-ancestors 'none'
- Content-Security-Policy: frame-ancestors 'self'
- Content-Security-Policy: frame-ancestors 'self' ℎttp://parent.utopia.net
- Content-Security-Policy: frame-ancestors 'self' ℎttp://parent.utopia.net ℎttp://grand-parent.utpia.net
<%@Page Language="C#"%>
<html>
<head>
<title>Parent</title>
<style>
body { font-size: 9pt; }
iframe {
display: block; margin: 12px; width:400px; height:60px;
margin-top: 2px;
}
.frame-title { margin: 0 12px; color: purple; }
</style>
</head>
<body>
<div>Parent </div>
<iframe src="http://child.utopia.net/aspnet/iframetest/child.aspx?fo=d"></iframe>
<iframe src="http://child.utopia.net/aspnet/iframetest/child.aspx?fo=s"></iframe>
<iframe src="http://child.utopia.net/aspnet/iframetest/child.aspx?fo=a"></iframe>
<iframe src="http://child.utopia.net/aspnet/iframetest/child.aspx?fa=d"></iframe>
<iframe src="http://child.utopia.net/aspnet/iframetest/child.aspx?fa=s"></iframe>
<iframe src="http://child.utopia.net/aspnet/iframetest/child.aspx?fa=a"></iframe>
<iframe src="http://child.utopia.net/aspnet/iframetest/child.aspx?fa=m"></iframe>
<script>
document.querySelectorAll("iframe").forEach(function(i) {
var d = document.createElement('div');
d.setAttribute('class','frame-title');
d.innerText = i.src;
i.parentNode.insertBefore(d, i);
});
</script>
</body>
</html>
測試一,ℎttp://child.utopia.net/aspnet/iframetest/parent.aspx 內嵌 ℎttp://child.utopia.net/aspnet/iframetest/child.aspx
parent.aspx 與 child.aspx 同來源,Chrome 部分除了 X-Frame-Options: DENY 及 Content-Security-Policy: frame-ancestors 'none' 被阻擋,其餘都能顯示。IE 則是 X-Frame-Options: DENY 及 X-Frame-Options: ALLOW-FROM ℎttp://parent.utopia.net 無法顯示(只允許 parent,不包含自己,也無法同時設二則),IE 不認識 CSP,CSP 的四條一律顯示。
測試二,ℎttp://parent.utopia.net/aspnet/iframetest/parent.aspx 內嵌 ℎttp://child.utopia.net/aspnet/iframetest/child.aspx
Chrome 的 X-Frame-Options: DENY、SAMEORIGIN 及 CSP frame-ancestors 'none'、frame-ancestors 'self' 被阻擋。IE 則是 DENY、SAMEORIGIN 被阻擋,CSP 全數顯示。
測試三,ℎttp://grand-parent.utopia.net/aspnet/iframetest/parent.aspx 內嵌 ℎttp://child.utopia.net/aspnet/iframetest/child.aspx
Chrome 的 X-Frame-Options: DENY、SAMEORIGIN 及 CSP: frame-ancestors 'none'、frame-ancestors 'self'、frame-ancestors 'self' ℎttp://parent.utopia.net 被阻擋,CSP frame-ancestors 'self' ℎttp://parent.utopia.net ℎttp://grand-parent.utpia.net 在測試二跟三都能顯示,證明 frame-ancestors 可支援多個 URL。但 X-Frame-Options: ALLOW-FROM ℎttp://parent.utopia.net 卻會顯示,證實 Chrome 不支援 ALLOW-FROM。
IE 則是 X-Frame-Options: DENY、SAMEORIGIN、ALLOW-FROM ℎttp://parent.utopia.net 都不顯示,ALLOW-FROM 限定 parent.utpia.net 有效。CSP 不支援故全數顯示。
測試四,CSP 萬用字元
另寫一個 wildcard.aspx,宣告 Content-Security-Policy: frame-ancestors ℎttp://*.utopia.net:
<%@Page Language="C#"%>
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
Response.Headers.Add("Content-Security-Policy", "frame-ancestors http://*.utopia.net");
}
</script>
<html>
<head>
<style>html,body {font-size:9pt;}</style>
</head>
<body>
CSP frame-ancestors http://*.utopia.net
</body>
</html>
測試由 localhost、child.utopia.net、parent.utopia.net、grand-parent.utopia.net 網頁內嵌它,除了 localhost 被阻擋外,其餘都可內嵌,驗證萬用字元有效:
最後一個問題,IE 只支援 X-Frame-Options,ALLOW-FROM 又只支援單一網址,要怎麼實現被一個以上網站內嵌?
我想了一個 Hacking 做法 - 偵測並檢查 Referrer,再動態吐回吻合 Referrer 的 X-Frame-Options ALLOW-FROM 網址,底下是個 PoC。
flex-allow-from.aspx 如下,它藉由 Request Referrer Header 偵測內嵌它的網頁來源,若其 Host 為 *.utopia.net,則將其做為 X-Frame-Options ALLOW-FROM 開放對象,否則傳回 DENY:
<%@Page Language="C#"%>
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
var referrer = Request.UrlReferrer;
var fo = "DENY";
if (referrer != null) {
if (referrer.Host.EndsWith(".utopia.net"))
{
fo = "ALLOW-FROM " + referrer.Scheme + "://" + referrer.Host;
}
}
Response.Headers.Add("X-Frame-Options", fo);
}
</script>
<html>
<head>
<style>html,body {font-size:9pt;}</style>
</head>
<body>
Simulate CSP frame-ancestors http://*.utopia.net
</body>
</html>
為確保 IFrame 載體網頁有送出 Referrer,特地寫了專屬 test-flex-allow-from.aspx,將 Referrer Policy 設成 origin:(提醒:放寬 Referrer Policy 會提高風險,請縮小套用範圍謹慎使用)
<%@Page Language="C#"%>
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
Response.Headers.Add("Referrer Policy", "origin");
}
</script>
<!DOCTYPE html>
<html>
<head>
<title>Parent</title>
<style>
body { font-size: 9pt; }
iframe {
display: block; margin: 12px; width:400px; height:60px;
margin-top: 2px;
}
</style>
</head>
<body>
<iframe src="http://child.utopia.net/aspnet/iframetest/flex-allow-from.aspx"></iframe>
</body>
</html>
實測結果如下,成功在 IE 實現類似 Content-Security-Policy: frame-ancestors ℎttp://*.utopia.net 的效果。
Experiments to show the behavior of X-Frame-Options and CSP frame-ancestors.
Comments
Be the first to post a comment