当前位置: 首页 > 科技观察

HTTP规范中那些隐藏的坑

时间:2023-03-14 17:43:25 科技观察

HTTP协议可以说是开发者最熟悉的网络协议了。“简单易懂”和“易于扩展”两大特点使其成为应用最广泛的应用层协议。虽然有很多优点,但是由于协议定义的玩法和限制很多,还是有很多不小心就会掉进去的隐坑。本文总结了HTTP规范中几个常见的暗坑,希望大家在开发中能够自觉规避,提升开发体验。1、RefererHTTP标准把Referrer写成Referer(少一个r),可以说是计算机史上最著名的错别字。Referer的主要作用是携带当前请求的源地址,常用于反爬虫、防盗链中。前段时间新浪油管挂图事件闹得沸沸扬扬,因为新浪油管突然开始检查HTTPRefererheader,非新浪域名不会返回图片,让很多中小博客闹得沸沸扬扬挂断。Referer虽然在HTTP标准中写错了,但是其他可以控制Referer的标准是不会出错的。比如禁止网页自动携带Referer头的标签,相关关键字的拼写正确:还有一个值得注意的是浏览器的网络请求。Referer等请求头从安全和稳定的角度来说,在进行网络请求时只能由浏览器控制,不能直接操作。我们只能通过一些属性来控制它们。比如Fetch功能,我们可以通过referrer和referrerPolicy来控制,他们的拼写是正确的:fetch('/page',{headers:{"Content-Type":"text/plain;charset=UTF-8"},referrer:"https://demo.com/anotherpage",//<-referrerPolicy:"no-referrer-when-downgrade",//<-});一句话总结:凡是涉及到Referrer的,除了HTTP字段是错误的,浏览器相关配置字段的拼写是正确的。2.空格1.%20or+in"Spiritual"?这是一个史诗般的坑,我曾经被这个协议冲突困了一天。在开始讲解之前先做个小测试。在浏览器中输入blanktest(blank和test之间有一个空格),看看浏览器是如何处理的:从动图可以看出,浏览器将空格解析为加号“+”。你觉得有点奇怪吗?我们再做一个测试,试试浏览器提供的一些函数:encodeURIComponent("blanktest")//"blank%20test"encodeURI("q=blanktest")//"q=blank%20test"newURLSearchParams("q=blanktest".toString()//"q=blank+test"代码不会说谎,其实上面的结果都是正确的,encode结果不同,因为URI规范和W3C规范冲突,所以会有这么扑朔迷离的乌龙事件。2.协议冲突我们先看URI中的保留字,它们不参与编码。有两种类型的保留字符:gen-delims::/?#[]@sub-delims:!$&'()*+,;是十六进制的,然后在前面加一个百分号。把空格这样的不安全字符转成16进制就是0x20,前面加一个百分号%就是%20:所以这时候再看encodeURIComponent和encodeURI的编码结果是完全正确的。由于空格转换为%20是正确的,转换为+时发生了什么?这时候我们就需要了解一下HTML表单表单的历史。早期的网页没有AJAX的时候,都是通过HTMLform表单提交数据。form表单的提交方式可以是GET或者POST。可以在MDN表单入口上测试:经过测试,我们可以看到在表单提交的内容中,空格被转换成了加号。这个编码类型是application/x-www-form-urlencoded在WHATWG规范中定义如下:这里基本解决了case,URLSearchParams是按照这个规范编码的。我找到了URLSearchParams的Polyfill代码,它将%20映射到+:replace={'!':'%21',"'":'%27','(':'%28',')':'%29','~':'%7E','%20':'+',//<=thisisthe'%00':'\x00'}specificationforthisencodingtype解释:application/x-www-form-urlencoded格式在很多方面都是一种异常的怪物,是多年实施事故和妥协的结果,导致了互操作性所必需的一组要求,但绝不代表良好的设计实践。特别提醒读者密切注意涉及字符编码和字节序列之间重复(在某些情况下是嵌套的)转换的扭曲细节。不幸的是,由于HTML表单的流行,该格式被广泛使用。这种方式不是一个好的设计。不幸的是,随着HTML表单的普及,这种格式已经普及。其实上面这句话有一个意思:这个东西设计成💩,但是很难返回3。一句话,在URI规范中,空格的encode是%20,而在application/x-www-form-urlencoded格式,空格编码为+。在实际业务开发中,最好使用业界成熟的HTTP请求库对请求进行封装,框架包办了一切杂务;如果你必须使用原生AJAX以application/x-www-form-urlencoded格式提交数据,不要手动拼接参数,使用URLSearchParams处理数据,这样可以避免各种恶心的编码冲突3、X-Forwarded-For获取的是真实IP吗?1.Story在本节开始之前,先讲一个开发中的小故事,可以加深大家对这个领域的理解。前段时间做一个风控相关的需求,需要获取用户的IP。开发后,对小部分用户进行了灰度化处理。测试发现后台日志中的灰度用户IP均异常。怎么会发生这样的巧合。测试后发了几个异常的IP:10.148.2.12210.135.2.3810.149.12.33...看IP特征就明白了。这些IP都是10开头的,属于A类IP(10.0.0.0-10.255.255.255)的私有IP范围,后台必须拿到代理服务器的IP,而不是用户的真实IP。2.原理现在一些大型网站基本都没有单点服务器。为了应对更高的流量和更灵活的架构,应用服务一般都隐藏在代理服务器后面,比如Nginx。加入接入层后,我们可以轻松实现多台服务器的负载均衡和服务升级。当然还有其他好处,比如更好的内容缓存和安全保护,但这些不是本文的重点。向上。网站加入代理服务器后,除了上述优点外,同时也引入了一些新的问题。比如之前的单点服务器,服务器可以直接获取用户的IP。添加代理层后,如上图,(应用)原服务器获取到代理服务器的IP。我之前讲的故事的问题就在这里。在Web开发这样一个成熟的领域,肯定有一个现成的解决方案,那就是X-Forwarded-For请求头。X-Forwarded-For是事实上的标准。虽然没有写进HTTPRFC规范,但从流行程度来说,其实也可以算是一个HTTP规范。本标准是这样定义的。代理服务器每次转发请求给下一个服务器时,都要将代理服务器的IP写入X-Forwarded-For,这样最终的应用服务收到请求时,会得到一个IP列表:X-Forwarded-For:client,proxy1,proxy2因为IP是一个一个推送进来的,第一个IP就是用户的真实IP,拿去用就行了。但就这么简单吗?3、攻击从安全的角度来说,整个系统最不安全的部分是人,用户端是最好破解和伪造的。一些用户开始利用协议的漏洞:X-Forwarded-For是代理服务器添加的。如果我在初始请求的Header中添加X-Forwarded-For,它不会欺骗服务器吗?1.首先,客户端发送一个带有X-Forwarded-For请求头的请求,其中包含一个假IP:X-Forwarded-For:fakeIP2。服务器第一层代理服务收到请求,发现已经有X-Forwarded-For:Forwarded-For,把这个请求误认为是代理服务器,所以在这个字段中加入了客户端的真实IP:X-Forwarded-For:fakeIP,client3.经过几层代理,服务器最终得到的header如下:将妨碍攻击者,您将获得fakeIP而不是客户端IP。4.如何破解诡计服务器?以上三步:第一步是客户端欺诈,服务器无法介入第二步是代理服务器,可控可防第三步是应用服务器,可控可防第二步是破招。例子。在最外层的Nginx上,我们配置X-Forwarded-For如下:proxy_set_headerX-Forwarded-For$remote_addr;这是什么意思?即最外层代理服务器不信任客户端的X-Forwarded-For输入,直接覆盖而不是追加。对于非最外层的Nginx服务器,我们配置:proxy_set_headerX-Forwarded-For$proxy_add_x_forwarded_for;$proxy_add_x_forwarded_for表示添加IP。通过这个技巧,可以破解用户端的伪造方法。第三步破招的思路也很容易。正常的思路是取X-Forwarded-For最左边的IP。这次我们反其道而行之。在下面的IP中,最右边的是真实IP。X-Forwarded-For:fakeIP,client,proxy1,proxy2比如我们知道代理服务有两层,从右往左数,去掉proxy1和proxy2,剩下的IP列表最右边就是真实IP。相关思路和代码实现请参考Egg.js前置代理模式。5.一句话总结在通过X-Forwarded-For获取用户真实IP时,最好不要取第一个IP,防止用户伪造IP。4.稍微容易混淆的定界符1.如果HTTP标准的HTTP请求头字段涉及到多个值,一般来说,每个值都是用逗号“,”隔开,即使是非RFC标准的X-Forwarded-For,values也是用逗号隔开:Accept-Encoding:gzip,deflate,brcache-control:public,max-age=604800,s-maxage=43200X-Forwarded-For:fakeIP,client,proxy1,proxy2因为一开始就用了逗号分隔值。当你以后想用另一个字段修改值时,分隔符变成分号“;”。最典型的请求头是Accept://q=0.9修改application/xml,虽然他们Accept:text/html,application/xml;q=0.9,*/*;q=0.8虽然HTTP协议很容易阅读,这种分隔符的使用不符合常识。按照常理,分号的断句语气比逗号强,但在HTTP内容协商的相关领域却相反。这里的定义可以看RFC7231,比较清楚。2、cookie标准与常规理解不同。Cookie实际上并不是HTTP标准。定义cookies的规范是RFC6265,所以分隔符规则不同。规范中定义的cookie语法规则如下:cookie-header="Cookie:"OWScookie-stringOWScookiecookie-string=cookie-pair*(";"SPcookie-pair)多个cookie之间用分号";"隔开而不是逗号“,”。随便拿起一个网站的cookies,可以看到是用分号隔开的。这里需要特别注意:3、一句话中,大多数HTTP字段的取值分隔符是逗号“,”Cookie不属于HTTP标准,分隔符是分号“;”5、文章推荐下面推荐我的一些文章:一篇介绍了webpack中最容易混淆的5个知识点,掘金快800赞了。一个意义不同的概念本文详细介绍了webpackdll是什么,并给出了2个摆脱繁琐dll配置的最佳实践ReactNative性能优化指南从渲染层的角度分析了RN性能优化的6点,并讲解了实现FlatList的原理图解。WebScraper-LightweightDataCrawlingTool推出一款浏览器爬虫小插件,可以实现简单的数据爬取功能