基於安全考量,現代網站通常會加上 HTTP Header X-Frame-Options 或 Content-Scurity-Policy(CSP) 防止 Clickjacking (點擊劫持)。(不知道 Clickjacking 的同學可參考 淺談 IFrame 式 Clickjacking 攻擊與防護,看一下其中「帥哥一秒變豬頭」的攻擊示範)

防範網頁被惡意網站內嵌成 IFrame 有兩種做法,有點過時的 X-Frame-Options (如果要支援 IE,這是唯一選擇),以及現代瀏覽器主打的 CSP frame-ancestors

應用時可分為以下三種情境:

  1. 禁止任何網站內嵌
    X-Frame-Options: DENY 或 Content-Security-Policy: frame-ancestors 'none';
  2. 限定只能被同網站內嵌(Scheme、Host、Port 都要相同)
    X-Frame-Options: SAMEORIGIN 或 Content-Security-Policy: frame-ancestors 'self';
  3. 只允許特定網站內嵌
    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,分別是:

  1. X-Frame-Options: DENY
  2. X-Frame-Options: SAMEORIGIN
  3. X-Frame-Options: ALLOW-FROM ℎttp://parent.utopia.net
  4. Content-Security-Policy: frame-ancestors 'none'
  5. Content-Security-Policy: frame-ancestors 'self'
  6. Content-Security-Policy: frame-ancestors 'self' ℎttp://parent.utopia.net
  7. 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

Post a comment