5

迷思:只要限定 POST 呼叫就不會有跨站台存取風險?

 3 years ago
source link: https://blog.darkthread.net/blog/xss-risk-of-post-only/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
迷思:只要限定 POST 呼叫就不會有跨站台存取風險?-黑暗執行緒

十二年前我寫過一篇 隱含殺機的 GET 式 AJAX 資料更新,說明「使用 GET 方式接收指令進行資料更新」的高風險,觀念還是對的,但隨時代演進,結論所說的「加上(Request.HttpMethod == "POST")的檢查阻止 GET 請求」具有防護效果在當年也許是對的,但當代瀏覽器支援跨來源資源共用(CORS),處理邏輯不同,只防堵 GET 或限定 POST 已不足以防止跨站台存取。

跨來源資源共用(CORS)規範,瀏覽器的 XHR 請求在符合以下條件時,會先送一個 OPTIONS 行前檢查請求(Preflight Request)確認是否對方開放 CORS 再正式發出 Request:(延伸閱讀:CORS OPTIONS Preflight Request 與 IIS 設定)

  1. GET/HEAD/POST 以外的請求
  2. 使用 POST,但使用 application/x-www-form-urlencoded, multipart/form-data, or text/plain 之外的 Content-Type,例如:以 POST 傳送 XML、JSON 等。
  3. 使用自訂 Header

然而,當未符合以上條件時,瀏覽器並非拒絕發送,而是直接發送請求,再檢查回應是否包含 Access-Control-Allow-Origin 等 Header 決定正常回傳結果或是觸發存取被拒錯誤。問題來了,若這是個更新作業 AJAX 呼叫,若伺服器端沒有妥善檢核請求發送方式,也沒有加上 CSRF / 跨站請求偽造防護,雖然瀏覽器會報錯並阻止 JavaScript 取得執行結果,但更新作業也已經跑完。

維基百科的這張流程圖可以看得更清楚,左下箭頭所指的 Make actual XHR,會造成未開放 CORS 的 API 也被執行:

用個實例驗證此點。為求簡便,我讓同一支 ASPX danger.aspx 依 mode 參數不同扮演三個角色:

  1. mode == "ajax"
    Guid.NewGuid() 產生隨機字串,寫入 Page.Cache["State"],這裡用 Request.HttpMethod == "POST" 限定 POST 請求 (注意:這是不夠的,將成為破口)
  2. mode == "check"
    顯示目前的 Page.Cache["State"]
  3. 其他情況預設顯示 HTML 介面,有一個 Button 呼叫 XHR 用 POST 方式呼叫 danger.aspx?mode=ajax,同時用 IFrame 內嵌 danger.aspx?mode=check 檢查 AJAX 更新是否成功

用先前介紹過的 Windows\System32\drivers\etc\hosts 多域名指向 127.0.0.1 技巧,我開啟 ℎttp://parent.utopia.net/aspnet/xss/danger.aspx 呼叫 ℎttp://child.uotpia.com/aspnet/xss/danger.aspx?mode=ajax,在 IFrame 則嵌入 ℎttp://child.uotpia.com/aspnet/xss/danger.aspx?mode=check 觀察 AJAX 更新結果。從 parent.utopia.net 呼叫 child.uotpia.com danger.aspx?mode=ajax 是一個跨來源呼叫,在沒設定 Access-Control-Allow-Origin Header 的情況下理應被瀏覽器擋下來。

<%@Page Language="C#"%>
<script runat="server">
void SetData(string value) 
{
	Page.Cache["State"] = value;
}

void Page_Load(object sender, EventArgs e)
{
	if (Request.HttpMethod == "POST" && Request.Form["mode"] == "ajax") 
	{
		//提醒:此為示範用途,未進行檢查即進行 AJAX 更新
		//實務上應增加 CSRF 防禦機制才合格
		SetData(Guid.NewGuid().ToString().Substring(0, 8));
		Response.Write("OK");
		Response.End();
	}
	else if (Request["mode"] == "check") 
	{
		Response.Write((string)Page.Cache["State"]);
		Response.End();
	}
	else {
		SetData("Empty");
	}
}
</script>

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<style>
			body,button { font-size: 9pt; }
			div { height: 20px; padding: 3px; }
			iframe { height: 40px; width: 150px; }
		</style>
	</head>
	<body>
		<button id=b onclick="testAjax()">Test Browser AJAX Call</button>
		<div id=m>Ready</div>
		<iframe id=f></iframe>
		<script>
			function showMsg(msg, noRefreshChk) {
				document.getElementById('m').innerText = msg;
				if (!noRefreshChk) {
					document.getElementById('f').src = location.href.split('?')[0] + 
						"?mode=check&t=" + (new Date().getTime());
				}
			}
			showMsg("Ready");
			var url = "http://child.utopia.net/aspnet/xss/danger.aspx";
			function testAjax() {
				var req = new XMLHttpRequest();
				req.addEventListener("load", function () {
					if (req.status == 200)
						showMsg("SUCC - " + req.responseText);
					else {
						showMsg("ERROR - " + req.status);
                    }
				});
				req.addEventListener("error", function () {
					//failed to get response from remote server
					showMsg("ERROR - Failed to send request");
				});
				req.open("POST", url);
				showMsg("Sending Request...", true);
				req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
				req.send("mode=ajax");				
			}
		</script>
	</body>
</html>

執行結果如下:

如以上所展示,parent.utopia.net 呼叫 child.utopia.net 的 POST 行為,雖然跨來源但 Content-Type 為 application/x-www-form-urlencoded,故不需要 Preflight Request,瀏覽器採取「先照常送出再檢查回應決定是否放行」策略,接著伺服器執行完回傳 HTTP 200,瀏覽器發現沒有 Access-Control-Allow-Origin 拋出錯誤:

Access to XMLHttpRequest at 'ℎttp://child.utopia.net/aspnet/xss/danger.aspx?mode=ajax' from origin 'ℎttp://parent.utopia.net' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. POST ℎttp://child.utopia.net/aspnet/xss/danger.aspx?mode=ajax net::ERR_FAILED 200

每次按鈕 Page.Cache["State"] 都會被更新,瀏覽器再回報 XHR 存取被拒,但木已成舟...

由此可知,瀏覽器只確保 JavaScript 拿不到未開放 CORS 的 AJAX 呼叫結果(用 F12 DevTool 可看到),在特定條件下會先發送再檢查 CORS 設定。

基於這個行為,伺服器端必須加入 CSRF 防護機制才能有效阻絕跨站台攻擊,以 ASP.NET 來說,不同版本都有內建相關 API:

除了引用現成機制,我們也可運用 CORS 原理,限定 POST 內容使用 JSON/XML 等 Content-Type 或要求自訂 Header,如此 JavaScript 端為送出有效 Request 就一定會觸發 OPTIONS Preflight Request 檢查,即可避免來自非 CORS 開放對象的 POST 請求降低風險。當然,這種做法的保護力比不上標準的 CSRF 防禦機制,但這裡仍會做個測試驗證效果。

<%@Page Language="C#"%>
<script runat="server">
//...略...
const string HeaderKeyName = "X-AJAX-KEY";
const string HeaderValueChk = "4890e7b7-5e60-4036-8ce4-cc56c79496de";
void Page_Load(object sender, EventArgs e)
{
	if (Request.HttpMethod == "POST" 
		&& Request.Form["mode"] == "ajax"
		&& Request.Headers[HeaderKeyName] == HeaderValueChk) 
	{
		//提醒:此為示範用途,未進行檢查即進行 AJAX 更新
		//實務上應增加 CSRF 防禦機制才合格
		SetData(Guid.NewGuid().ToString().Substring(0, 8));
		Response.Write("OK");
		Response.End();
	}
	//...略...
}
</script>

<!DOCTYPE html>
<html>
    <!-- ...略... -->
		<script>
		    //...略...
			var url = "http://child.utopia.net/aspnet/xss/safe.aspx";
			function testAjax() {
				var req = new XMLHttpRequest();
				//...略...
				req.open("POST", url);
				showMsg("Sending Request...", true);
				req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
				// 設定 Header 設定
				req.setRequestHeader("<%=HeaderKeyName%>", "<%=HeaderValueChk%>");
				req.send("mode=ajax");				
			}
		</script>
	</body>
</html>

實際測試,POST Request 便會因 Preflight OPTIONS 檢查失敗無法送出,不會執行更新。

結論 - 擋掉 GET 限定 POST 現今已無法有效阻止跨站台存取,請確實引用 CSRF 防禦以保安康。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK