WEB安全 第五课 HTTP协议 之一 HTTP基本语法 |
我们要讨论的下一个基本概念是超文本传输协议(Hypertext Transfer Protocol,HTTP):它是Web的核心传输机制,也是服务器与客户端之间交换URL引用文档的首选方式。尽管名字里包含了超文本这几个字,但HTTP和真正的超文本内容(HTML语言)其实彼此独立。当然在某些时候,它们会以一种令人意想不到的方式交织在一起。
HTTP的历史很有意思地展示了其创立者的抱负和Internet的发展。Tim Bemers-Lee在1991年发表的最初版本的协议草案(HTTP/0.9)只有不足一页半的内容。它甚至没有包含一些在日后明显非常必要的需求,如对传输非HTML类型数据的扩展支持。 🧑⚕️🩲💿😷🖕 在历经5年的时间和若干版本的更替后,第一个正式版HTTP/1.0标准(RFC 1945^2)诞生了,这时它已经变成一份密密麻麻长达50页的文档,期望能弥补过往标准里的诸多缺陷。很快到了1999年,HTTP/1.1(RFC2616^3)的7位署名作者显然指望该协议能涵盖到方方面面,导致的结果就是一份长达150页的大作。 但这些越来越宏伟的文档里很大篇幅的内容,和我们当下实际在用的Web并非特别相关,因为它们对新功能的追逐远比修复旧有缺陷更感兴趣。 现在所有的客户端和服务器端都兼容HTTP/1.0协议(但这种兼容又没有完全照搬HTTP/1.0协议,而是或多或少有些变化);除了在某些扩展属性上有例外的情况,大多数产品都较为合理地支持完整的HTTP/1.1协议。尽管没什么实际意义,但部分Web服务器和所有常见的浏览器,也向下兼容HTTP/0.9协议。🧒👔💊🤮🤙 1.HTTP基本语法 如果只是匆匆一瞥,HTTP相当简单。HTTP协议建立在TCP/IP传输方式之上,基于文本方式进行交互。每个HTTP会话会先与服务器端建立一个TCP连接,通常连接80端口,然后对要获取的URL发出具体的请求信息。而在响应时,服务器端会返回被请求的文件内容。通常情况下,服务器端随后会断开TCP连接。 🧑🚀🩲⚒😂🤞 初始版本的HTTP/0.9协议完全没有为客户端和服务器提供任何额外的元数据交换空间。它的客户端请求仅有一行,以GET开头,后面跟着URL路径和查询字符串,然后以一个CRLF换行符结束(对应的ASCII码为OxODOxOA;按照规范如果结尾处只有一个LF字符,服务器也会接受)。一个HTTP/0.9请求样例如下: GET /fuzzy_bunnies.txt 服务器对这条请求的回应,是立刻返回它需要的HTML数据(按照规范,服务器应该按每行80个字符换行的方式来返回响应的内容,但基本上没人遵守该条建议)。👨🚒👠🔑😈👀 HTTP/0.9的处理有一堆严重缺陷。例如,没法根据浏览器里用户的首选语言或支持的文档类型等进行交互处理。当找不到请求的文件需要被重定向到新的位置,或返回的内容不是HTML格式的文件时,在HTTP/0.9协议里服务器也没法通知到客户端。 最后,这个协议对网站管理员也非常不友好:因为传输的URL信息里只包括了路径和请求行,所以不能做到在一台服务器上用一个IP地址支持多个不同主机名的网站——有别于域名,IP地址可是比较珍贵的资源。 👒💶😳✊ 为修正这些缺点(也为了日后有更多的改进余地),HTTP/1.0和HTTP/1.1的格式就略有变化了:请求的第一行包含具体的版本信息,后面则跟着零到多条以“名称:值”(name:value)格式组成的数据行(也叫头域,Headers),每行为一个头域。常见的请求头包括:User-Agent(浏览器版本信息)、Host(URL主机名称)、Accept(支持的MIME文件类型)、Accept-Language(支持的语言编码)和Referer(这个字其实拼错了,它的含义是,如果有的话,显示发起当前请求的原始页面)。 请求头的部分最后以一个单独的空行结束,这个空行后面可以再跟上客户端希望发送给服务器的任何数据(这些数据的长度必须以Content-Length请求头明确标识)。HTTP协议本身对这段数据的内容格式其实并未做任何规定;在HTML页面里,这个位置通常用做表单数据传递。传递的方式有好几种可能的格式,当然这些都不是硬性的规定。 总的来说,一个简单的HTTP/1.1请求如下: 🤙⛵🥚❗🐖 POST /fuzzy_bunnies/bunny_dispenser.php HTTP/1.1 Host: User-Agent:Bunny-Browser/1.7 Content-Type:text/plain 👏🌡🎂‼🪶 Content-Length:17 Referer: I REQUEST A BUNNY 👮♂️🩳📠👍 服务器对这个请求的响应是,第一行包含支持的协议版本和一个数字格式的状态码(用以显示出错状况和其他特殊情况),还有可选的易于理解的状态信息描述。紧随其后的是若干一看自明的响应头,响应头部分以一个空行结束。响应数据的最后再跟着请求资源的具体内容。 HTTP/1.1 2000K Server:Bunny-Server/O.9.2 Content-Type:text/plain👳👓📏😂💪 Connection:close BUNNY WISH HAS BEEN GRANTED RFC2616还规定,除非客户端明确地发过来一个Accept-Encoding头,在里面设定了能接受的传输压缩方式,否则服务器端可以在以下三种压缩方式里(gzip、compress和deflate)任选其一,用于传输响应数据。 🤛🌕🍪♏🐝 #f187: 2.支持HTTP/0.9的恶果 HTTP/1.0和HTTP/1.1协议已经做了很多改进,而大家对古老“愚笨”的HTTP/0.9协议都很不待见,我们平时也根本意识不到它的存在,但它却并未完全退出历史舞台。 🦴🚗🔪🆗🦬 部分原因要拜HTTP/1.0规范所赐,因为该规范要求以后所有的HTTP客户端和服务器都需要支持这个半成品级别的草案。特别是,在第3.1节里规定如下: HTTP/1.0客户端必须……能够理解任何有效的HTTP/0.9或HTTP/1.0.格式的响应。 ✌🗼🍓🔞🦌 到最近几年,RFC2616对这项要求也颇有悔意(第19.6节说:“协议规范并不强制要求兼容更早期的版本”),但因为那个更早期的建议,我们现在用的各种浏览器仍然继续支持这个古老的协议。 要理解这个模式为什么有危险,让我们来回顾一下HTTP/0.9协议:它返回的内容只有请求的文件本身。没法从这些回应的内容里,表明响应方确实是理解HTTP协议的,以及返回的内容是否真的为HTML文件。谨记这一点,让我们来分析一下,如果服务器example.com的25端口有上运行着一个不做任何检查的SMTP服务,浏览器向这个端口发送了HTTP/1.1请求会有什么效果: GET /<html><body><hl>Hi! HTTP/1.1👨⚕️🥼🩸😔✋ Host:example.com:25 因为SMTP服务器不理解这个请求是什么意思,它很可能这么回应: 220 example.com ESMTP 500 5.5.1 Invalid command:"GET /<html><body><hl>Hi! HTTP/l.l"🧑🌾🛍🔌🥰👃 500 5.1.1 Invalid command:"Host:example.com:25" .... 421 4.4.1 Timeout 只要是遵守RFC协议的浏览器,都会被迫接受返回的信息,认为这些就是有效的HTTP/0.9响应内容,并把返回的文档按HTML格式解析。所以浏览器会把上面那些由攻击者控制,出现在返回错误信息里的代码,解析为example.com网站返回的合法网页内容。与浏览器安全模型的深层交互会在本书第二部分再详加讨论,总之,后果可以很严重。 👌🗼🍇☯🦊 #f196: 3.换行处理带来的各种混乱 先把HTTP/0.9和HTTP/1.0的巨大差异放到一边不说,这些版本的HTTP协议还加了几项核心的语法调整。最著名的可能是,有别于之前的迭代版本,HTTP/1.1要求客户端不仅要接受CRLF和LF格式的换行符,还要接受单个的CR字符。尽管最常见的两种Web服务器(IIS和Apache)都不接受RFC规范里这条建议,但除Firefox以外所有的客户端浏览器都遵从这一规定。🧒🎩🪥😘🤳 这种不一致性使得开发人员在处理HTTP头域里任何攻击者控制内容时,不仅要过滤LF字符,其实还要过滤CR字符。要说清楚这个问题,以下面这个服务器响应为例,其中加粗部分就是出现在响应头里未经足够过滤的用户提供内容: HTTP/1.1 200 OK[CR][LF] Set-Cookie:last_search_term=[CR][CR]<html><body><hl>Hi![CR][LF] ✋🗼🥑🉑🐂 [CR][LF] 动作完成。 对IE浏览器来说,这个响应看起来类似: 🤝🚠🌰❓🕊 HTTP/1.1 200 OK Set-Cookie:1ast_search_term= <html><body><hl>Hi! 🧥💊🥱✊ 动作完成。 实际上,这类在HTTP头域里夹带换行符的漏洞,不管是因为数据解析的不一致性还是由于没有正确过滤各种类型换行符而产生,都实在是太常见了,所以这类攻击还有专门的名字:头域注入(Header Injection)或者响应拆分(Response Splitting)。 另一个有潜在安全风险但较不为人知的问题是:由于HTTP/1.1协议里的规定,所以实际上HTTP协议支持多行请求头,它规定任何一个以空格开头的行,都是接续在前一行之后的内容。例如: 👂🌡🍒❌🐉 X-Random-Comment:这是个长句子, 所以我们得换行处理一下,看着更整齐。 IIS和Apache都接受由客户端程序发出的多行格式请求头,但实际上,IE、Safari或Opera都不支持发出这种请求头格式。在攻击者控制的环境里,某些具体实现如果必须釆用多行标头,或者允许采用多行标头那就可能导致问题。但谢天谢地,这种攻击能发挥作用的地方极少。 👁🚐🍼❌🦊 #f473: 4.经过代理的HTTP请求 许多机构和ISP供应商都会利用HTTP代理,以其用户的名义提供对HTTP请求进行拦截、检查和转发等功能。使用代理的原因可能是为了提高性能(先把某些服务器的响应缓存到就近的系统)、或强制应用某些网络访问策略(如禁止访问某些色情站点)或需要以代理方式实现某些独立网络环境的监控和需要授权的接入。 🧓🧦🗑🤞 常规情况下HTTP代理需要浏览器的支持:浏览器要先修改配置,要求把经过修改的请求先发往代理服务器,而不是直接和目标地址进行连接。要使用代理方式来获取一个HTTP资源,浏览器通常需要发送如下请求: GET HTTP/1.1 User-Agent:Bunny-Browser/1.7 👌🏝🍞🈚🐤 Host: ... 上述例子和普通的HTTP请求最大的语法差异,就是请求内容里的第一行,这时候是一个完整的URL( ) 通过这项信息代理服务器才知道用户要连接的目标服务器在哪里。这项信息实际上有点多余,因为在Host请求头里也标识了主机名称;这种重复是因为这两套机制其实是相互独立发展起来的。 ✌🎢🍼🆘🦕 为了避免客户端和服务器串通一气,如果Host请求头的信息与请求行里的URL不匹配时,代理服务器应以请求行里的URL为准,或者用特定的“URL-Host”数据对和缓存内容关联起来,而不能只根据其中一项信息做出判断。 许多HTTP代理服务器还允许浏览器获取非HTTP类型的资源,如FTP文件或目录。这种情况下,在把数据返回给用户之前,代理服务器会把HTTP响应里返回的内容先封装整理一下,可能会先把它们转化成HTML格式也就是说,如果代理服务器不理解所请求的协议,或者代理服务器不便于查看交换的数据时(例如,处理的是加密会话),就必须另作处理了。 Connect请求方法就是为这些特殊的连接而准备的,但这种请求方式在HTTP/1.1RFC里并没有更进一步的描述。而与之相关的请求语法却放在另一份1998年草拟的独立规定里。CONNECT方法的格式如下:🧑⚕️👒🧲😇✋ CONNECT HTTP/1.1 User-Agent:Bunny-Browser/1•7 ... 👀🎢🔪🆎🐖 如果代理服务器同意并且能够和请求的目的端地址连接上,它会为这个请求返回一个特定的HTTP响应码,Connect协议的工作也就告一段落了。这时候浏览器开始建立TCP流,直接向代理服务器发送和接收原始二进制数据;相应地,代理服务器也会不加选择地直接转发两个端点之间的数据流。 注意滑稽的是,这个规范里有一个很微妙的疏忽,许多浏览器在使用加密连接时,会错误地处理从代理服务器自身返回的非加密错误响应信息。浏览器错误地认为,这些纯文本格式的响应就是从目的服务器的加密通道返回的。由于这个小疏漏,原本在Web上使用加密通信应有的保障,就全被破坏掉了。 (这种情况下,一些由客户端提供的HTTP请求头就只供代理服务器内部使用了,而不会再被发往非HTTP协议的另一端,这会导致一些未必与安全相关但颇为有趣的协议含糊问题。存在长达10年之久才被发现和纠正)。 🤟⛪🥑♂🐒 还有一些不使用HTTP方式和浏览器打交道的更为底层的代理形式,这些代理为了缓存内容或强制执行某些规则,也会需要检查HTTP的信息交换。一个经典的例子就是透明代理,它默默地在TCP/IP层拦截数据流量。透明代理所用的方式一般都有缺陷:代理服务器能看到拦截的请求里目标端IP和主机头Host信息,但没有办法立刻确认,要连接的目标端IP是否真的和Host里设定的服务器名称相匹配。 除非额外先做一次查询,确定两者是否真的相关,否则两边串通一气的客户端和服务器就有机可乘了。如果不先做额外的检查,攻击者只需要请求连接自己家中的服务器,但发送的是一个故意误导代理的Host请求头: ,这样其他的确想访问 的用户,获得的可能就是被错误缓存的响应内容了。 🖕⛪🧊🅱🐝 #f184: 5.对重复或有冲突的头域的解析 尽管RFC2616已经写得相对详尽,但对一个符合要求的解析器要怎么解析请求或响应数据里含糊和有冲突的数据,并没有做更详细的描述。这个RFC文档的第19.2节“宽容的应用程序”里建议在某些“无歧义”的场景里对某些特定的值,可采用较为宽松和对错误较为容忍的解析方式,但这个用词本身,就很“有歧义”啊。 🤌🎢🥭🈴🪶 例如,因为规范里没有给出建议,大约一半的浏览器会以HTTP头域里第一次出现的值为准,而另一半则以最后一次为准,这使得每种和头域相关的注入漏洞,不管怎么限制,总有另一部分的用户会遭殃。而在服务器端,也是一样地随机:Apache认可第一个Host头的值,而IIS则完全不接受多个Host头的情况。 与之类似的情况,相关的RFC文档也没有明确规定要禁止可能有冲突的HTTP/1.0和HTTP/1.1请求头,也没有要求HTTP/1.0版本的服务器或客户端程序必须忽略所有HTTP/1.1的语法。因为这种设计,很难预料如果同时具有HTTP/1.0和HTTP/1.1两个版本的不同名称但一样功能的头域时,会产生什么后果,例如Expires和Cache-Control。 最后,要如何解析某些较为罕见的头域冲突,在规范里的描述反而非常清楚,但为什么要允许这些奇怪的冲突也令人费解。例如,HTTP/1.1客户端在每个请求里,都要发送Host主机头,但也规定了服务器(并非只有代理服务器)端必须能识别在HTTP请求第一行里的绝对URL路径,而常规的HTTP请求第一行里只有路径和查询字符串等相关参数。这让人很好奇要如何解析以下请求:🕶🧬😡🙏 GET HTTP/1.1 Host: 在这种情况下,RFC2616的5.2节要求客户端不要理会非功能性的Host请求头(但这个请求头都必须存在!),大多数客户端程序也遵从这条建议◦但问题是,某些底层应用可能不太清楚这条吊诡的要求,而选择根据请求头里的Host头来做判断。 👆🚂🫖🈚🦠 注意:当我们抱怨HTTPRFC文档的各种疏漏时,也得承认即使有额外的规范,也同样会面临各种问题。比如在RFC描述某些场景时,希望对一些很罕见的情况也进行明确的强制性处理,这会导致在模式解析上变得非常荒谬。例如RFC1945的3.3节,就对某些HTTP头域里的时间日期解析提出了建议。这导致为实现这个建议,Firefor代码库7里的prtime.c文件长达2000行,这段C代码既难懂又难读,而目标仅仅是为了实现足够的容错,以便解析各种日期、时间和时区格式(用于确定缓存内容是否过期)。 #f472: 6.以分号作分隔符的头域值 🧒👖📱😚💪 有一些HTTP头域,例如Cache-Control或Content-Disposition,使用分号来分隔在同一行里的几对独立的“名称=值”数据组。允许使用这种嵌套式语法的原因,人们可能认为这种方式更有效率或者更直观一些,否则就要用到好几个独立的HTTP头域了。 RFC2616规定在某些使用场景里,这种数据组对里等号右边的参数值可以使用Quoted-String格式。Quoted-String指的是用双引号作为分隔符,由任意可打印字符组成,两边用双引号括起来的字符串。当然了,缺点是引号本身不能包括在字符串里,但好处是支持分号和空格,使得某些不加引号可能会有问题的字符串仍能保持原样传输。 🖐🗼🍏♑🐝 但很遗憾,对开发者来说,IE浏览器对Quoted-String的语法支持得不是很好,直接导致这种编码策略完全派不上用场。IE浏览器对以下数据的解析完全出人意料(这行的本意是表明当前返回的是一个要下载的文件,而不是直接显示在线阅读的文档):Content-Disposition:attachment; fi1ename=”evil_file.exe;.txt“ 在微软的实现里,文件名会在有分号的地方被截断,导致文件最后被存为evil_file.exe。应用程序在处理这样的文件名时,必须要专门检查整个字符串中的双引号和换行符,否则仅仅去检测后缀名是否“安全”或人为添加一个“安全”的后缀名,都避免不了风险隐患。🧑🚀🩲🧪🥲 另外要值得注意的是,在同一个HTTP头域里,以分号分隔的两个字段,如果名称完全一样,那应该如何处理,RFC里并没有规定这种情况下的解析顺序。以Content-Disposition里的filename=为例,所有的主流浏览器都以第一次出现的值为准。但其他情况的处理就未必这么统一了。例如,要解析Refresh响应头(用于强制在若干时间内重定向到新页面)里的URL=值时,IE6以最后一个值为准,而其他浏览器都取第一个值。而处理Content-Type响应头域时,Internet Explorer、Safari和Opera都会采用第一次出现的charset=数值,而Firefox和Chrome则会取最后一次。 #f472: 7.头域里的字符集和编码策略🧑🎤👜🎺🤩✋ 和URL处理的相关规范类似,HTTP规范里也基本回避了在头域的值里出现非US-ASCII字符时要如何处理的问题。某些含糊的场景里也许可以出现合法的非英文字符(例如,在Content-Disposition的文件名filename项里),但要真碰到这些情况浏览器要如何应对却没有什么明确规定。 最初,RFC1945允许TEXT标记中包含8比特字符,并对TEXT的类型给出了如下定义(TEXT标记是广泛用于定义其他字段语法的基元): 🧑🍳🩳💊😡🤟 OCTET:任意的8比特序列字符 CTL:US-ASCII码表里的所有控制字符,包括八进制的0-31和DEL(127) TEXT:就是除CTL控制字符之外的所有OCTET字符,但还包括LWS字符 但之后RFC又提出了一个很古怪的建议:如果在TEXT的字段里碰到非US-ASCII的字符,客户端和服务器可以按ISO-8859-1格式(也即标准的西欧语言代码页)进行解析,但这条无需强制执行。然后,RFC2616复制黏贴了这段关于TEXT类型的模式规范,但加了个备注,说非ISO-8859-1的字符串必须按照RFC20478规范里的格式先进行编码,而RFC2047原本是用于电子邮件通信的。👵👖📐🤮🙏 这下热闹了;在这个简单的协议里,要编码的字符串得先以“=?”做前缀,然后跟上具体编码字符集的名称,再跟上一个“?q?”或“?b?”,标识其所用的编码方式(两者分别代表Quoted-Printable或Base64编码方式),然后才是经过编码后的字符串内容。这串信息的最后还要再跟上一个“?=”作为结束符。举例如下: Content-Disposition:attachment;filename="=?utf-8?q?Hi=21.txt?=" 🧑💻👚🪦😈🤌 注意:RFC理应规定在相关的头域里不能出现任何假冒的“=?…?=”信息,以避免对实际上压根没经过编码的数据进行完全不必要的解码处理。 遗憾的是,对RFC2047规定的编码方式的支持寥寥可数。在Firefox和Chrome的某些头域里能识别这种编码方式,但其他浏览器就完全不配合了。IE浏览器能识别Content-Disposition域里URL风格的百分号编码方式(Chrome也釆用这种方式),并且默认为UTF-8格式编码。而另一方面Firefox和Opera则选择支持RFC22319里提出的一种很特别的百分号编码语法,和HTTP常规语法相比,这变化实在有点大: Content-Disposition:attachment;filename*=utf-8'en-us'?Hi%21.txt ✌🗺🧊♊🦬 聪明的读者可能会留意到,没有任何一套编码方式能同时为所有浏览器支持。所以一些网站应用开发人员就干脆直接在HTTP头域里使用高位字节数据,一般为UTF-8编码格式,但这么做在一定程度上也不太安全。例如Firefox里,如果在Cookie头域里存放UTF-8文本的话,长期以来就有乱码的问题,这会导致如果攻击者在Cookie里进行注入攻击,可能会出现难以预料的分隔符问题。 说到字符编码问题,对NUL空字符(0x00)的处理可能也需要提一下。这个字符在很多编程语言里都作为字符串结束的标志,理论上来说,它就不应该出现在任何HTTP头域里(除非出现在之前提到的不太好用的Quoted-Pair语法环境里),但我们也要记住,解析器总是尽量容错的。如果允许出现这个字符,它很可能会产生意想不到的副作用。例如,Content-Disposition请求头里,如果出现NUL字符,IE浏览器、Firefox和Chrome都会做出字符截断的处理,而Opera和Safari则不会这么做。 🧒👚💊💩💪 8.Referer头域的表现 本章早前提到,HTTP请求里可以包含Referer(来源)头域。这个头域里包含的是哪个URL地址触发了对当前页面的访问。 Referer头域对某些纠错处理颇有帮助,还可以通过这个头域,强化网页之间的引用关系,有助于Web的发展壮大。但遗憾的是,这个头域也可能向某些心怀恶意的家伙泄漏用户的浏览习惯,还可能暴露引用页面的查询字符串参数,这些经过URL编码的参数里面有可能包含敏感信息。 👀⛪🍞♏🐥 因为担心这个问题,同时缺乏解决这个问题的有效建议,这个头域经常在安全控制或策略强制时被错误使用,但实际上它承担不了这种重任。主要是因为我们没办法主动区分以下几种缺乏Referer头域的情况:客户端根据用户的隐私策略设置,直接就没有发出这个头域;用户的浏览行为确实没有涉及这个头域;该请求的来源是恶意站点,它刻意屏蔽了这项信息。 正常来说,这个头域在大部分HTTP请求里都会出现(在HTTP级别的跳转里也会保留下来),但以下情况除外: 👂💈🍪✔🐖 □刻意在地址栏里直接输人一个新的URL或从书签里打开网页 □浏览的动作是由一个伪URL文档,如data:或javascript:触发的 □当前请求来自Refresh响应头(并非基于Location的重定向方式) 🧑💻🩰🪓😃🤳 □当来源站点是加密站点而请求的是非加密页面时。根据RFC2616的15.1.2节规定,这是出于隐私保护的考虑,但这样做其实没太大意义。当用户从加密的域名浏览到另一个无关的加密网站时,Referer信息还是会泄漏给第三方,所以加密也并不等于安全啊。 □用户可以通过调整浏览器设置或安装隐私保护插件,选择不发送或干脆伪造一个来源页面。 👍🗼🍞♑🦦 很明显,以上5种情况里有4种情况都可能是恶意站点蓄意诱发的。 WEB安全第五课第二节:
帖子热度 1.4万 ℃
|
|
|