WEB安全 第四课 对web的分析 之 一切从URL开始 |
最易辨识的Web标志就是URL(Uniform Resource Locator,统一资源定位器)了,它由一串很简单的文本字符组成。一条格式正确且符合规范的URL必然对应着远程服务器上某个独一无二的资源(在这个解析的过程中,还需要实现另外一些相关的功能)。URL语法规范是浏览器地址栏的基础,而地址栏中的信息正是每个浏览器用户界面中最重要的安全标识。
![]() 除了内容检索时用到的真正的URL,还有几种语法与之相类似,但用于浏览器端功能的伪URL,这些功能包括内置的脚本引擎、几种特别的文档渲染模式及诸如此类的功能。所以毫无疑问,这些伪URL对应的动作对链接了它们的站点有重大安全影响。 👂🍟❎🐻 搞清楚浏览器对URL的解析机制以及它们带来的副作用,是我们和Web应用都需要面对的最基础且最常见的安全问题,同时URL机制自身也是错漏百出。URL的语法是由TimBemers-Lee制定的。在Web里的实际运用请参见RFC1738、2616以及若干相对次要的标准文档。这些文档都非常详细,这导致URL的解析模式相当复杂,但它们又未能足够精确地描述出在客户端软件里需要怎样实现URL机制才能做到既兼容又准确。此外,各家软件开发商出于各自的考虑,都会稍许偏离这些规范。让我们来仔细看一下貌似简单的URL在实际环境中是如何工作的。1.URL的结构 下图显示的是一个符合规范的绝对(absolute)URL,它包括了访问某特定资源所需要的全部信息,绝对URL和访问时的状态完全无关。与之相对应的是省略了部分信息的相对(relative)URL,如../file.php?text=hello+world,它需要根据当前浏览所在上下文环境里的基准URL,才能确定完整的URL地址。 🧠🏫🍇🐟 ![]() 绝对URL的各个部分看起来都还算一目了然,但每个部分都有一些值得注意的问题,让我们来回顾一下吧。 2.协议名称 👎🎢🍪🅿🐝 协议名称由一串不区分大小写的字符串组成,以单个冒号结束,表明获取该资源时需要使用的协议。官方认可的有效URL协议统一由IANA(Internet Assigned Numbers Authority,互联网数字分配机构)维护,该机构更广为人知的功能是管理IP地址空间。IANA当前的有效协议名包括http:、https:和ftp:等几十项,而实际上,常用浏览器和第三方应用往往还支持若干额外的协议,其中一些还会带来安全问题。(特别值得关注的是几种伪URL,如data:和javascript:,在后续内容里会有详细的讨论。) 在浏览器和Web应用要做进一步解析之前,它们还得先区分要处理的是完整的绝对URL还是相对URU在URL地址的最前面是否包含有效的协议名称原本是最关键的区别,RFC1738里是这么定义的:绝对URL应遵守以下规定,在冒号“之前,只能出现字母数字和“+”、“和这几个符号。但在实际环境里,每种浏览器都稍许偏离了这个指引,它们全都会忽略前导换行符和空格。IE还会忽略所有的不可打印字符(ASCII代码表里0x01〜OxlF之间的字符)。在此基础之外,Chrome更是会忽略0x00和NUL空字符。大多数浏览器的具体实现还会忽略在协议名中出现的换行符和制表符,而Opera浏览器还接受在协议字符串出现高位字符。 🤞🦼🍓🔞🦖 由于这些不兼容性,那些需要区分相对和绝对URL的Web应用就必须谨慎地拒绝任何异常的语法。但我们很快就会发现,即使这样做了也还是不够的。3.层级URL的标记符号 根据RFC1738规定的语法,在授权信息之前,每个层级结构的绝对URL里都应该包括固定的字符串“//”。按这个规范的意思,如果没有这个字符串,就无法确定URL后续部分的格式和功能了,只能把它们看成一个含糊的与特定协议相关的值。🧑⚕️🩳🔌😈🖕 注意:一个非层级结构的URL例子就是mailto:协议,它用于指定电子邮件地址,可能还包括主题信息(mailto:[email protected]?subject=Hello+world)。 这样的URL会被传递到默认邮件客户端程序而无需经过其他解析。 👈🎠🦀❗🐠 理论上,这种统一的层级URL语法的概念确实很优雅。因为应用可以无需关注某个协议的具体实现,就能够提取到只和地址有关的信息。例如,看到某种特定的名为wacky-widget:的协议,浏览器就能确定 和wacky-widget://example.com/test2/ 指向的是同一个受信任的远程主机。 但相当遗憾的是,这个规范包含着一个有意思的缺陷:上面提到的那个RFC文档并没有指明,如果碰到一个非层级结构的URL又带有“//”前缀,当做何处理;反之亦然。在更早期的RFC1630协议里用作参考的URL解析器实现就无意中包含着这个漏洞,这导致后来与URL有关的处理都有问题。在若干年后发表的RFC3986里,作者出于兼容性的考虑,懦弱地接受了这些漏洞并允许解析这类URL。这样导致的后果就是,各家浏览器对以下这些例子的解析让人很摸不着头脑:#y450: ①http:example.com/当没有符合要求的基准URL环境时,在Firefox、Chrome和Safari里这个地址会被认为等同于 如果有基准URL,则会认为这是一个指向目录example.com的相对路径。👳🧢🩺😷🤛 ②javascript://example.com/%OAalert(l)在所有常用浏览器里,会认为这个字符串是一个有效的非层级伪URL并以JavaScript方式来执行alert(l)这段代码,显示一个简单的对话框。 ③mailto://[email protected] IE认为这是一个有效的指向电子邮件地址的非层级URL;“//”的部分会直接被忽略掉。而其他浏览器则不认可这个写法。 🧑💻🩳💶🥰👃 4.访问资源的身份验证 URL里身份验证的部分属于可选项。在向服务器端获取数据时,有可能需要在该位置指定一个用户名或密码。但这个抽象的URL语法本身,与具体的用户名密码等身份验证信息的交换并无实质性关系,身份验证信息的传输是和协议相关的。对那些不支持身份验证的协议,如果在URL里强行加入这部分信息该做何处理,协议并未做出规定。 如果没有提供身份验证信息,浏览器默认以匿名的方式获取数据。在HTTP和其他几种协议里,这意味着没有传送任何身份验证信息;对FTP协议,这包含着一个名为ftp的账号和一个假的密码。 💪🎢🌶➡🐥 除常规的URL分隔符之外,大多数浏览器对身份验证部分的数据几乎接受任何字符,但有两个例外:出于某些尚未明了的原因,Safari拒绝了许多字符,包括和“厂,而FireFox还拒绝换行符。 5.服务器地址 👩👠📮🤖🧠 对完整的层级URL来说,服务器地址部分必须指定一个不区分大小写的域名(例如example.com)、一个IPv4地址(例如127.0.0.1)或在一对方括号里的IPv6地址([0:0:0:0:0:0:0:1]),用以标识请求资源所处的服务器位置。FireFox还接受写在一对方括号里的IPv4地址和主机名,而其他浏览器则会拒绝这种写法。 尽管RFC里只允许符合规范的IP地址写法,但大多数应用所依赖的标准C类库却比较灵活,可以接受八进制、十进制和十六进制的写法,甚至可以接受把其中几个或全部8位元数据(Octet)拼在一起再转成单个整数的写法。所以以下的各种写法实际上是一样的: ① 这是IPv4地址的正规写法。 💅🏝🦞🆘🦮 ② 同一个地址,但先以十六进制数字表示标准写法里的第一个8位元(Octet),剩下的3个8位元数据先分别按十六进制的格式拼在一起,再整体地转化成一个十进制的整数。 ③ 同一个地址,以0为前缀,后面是把全部4个8位元数据的十六进制数值拼在一起,再统一转化成单个八进制整数。 🦷🛩🥣📳🪰 这种很随意的解析方式也相似地出现在域名里。理论上来说,域名里能用的字符集是很有限的(按照RFC1035的定义,应该只能出现字母和数字、“.”和“-”),但很多浏览器查询任何信息几乎都依赖于操作系统的解析器,而操作系统的处理通常不太严谨。各种客户端能接受以及传递给系统解析器的主机名(Hostname)字符集也都不尽相同。Safari最严谨,而IE宽容度最高。注意多数主流URL解析器都有一个令人诧异的行为,它们都会主动地把主机名里的全角句号“。”(在字面上表示结束,对应的Unicode代码是U+3002)直接替换为点号,但这种情况如果出现在URL的其他部分里则不会这样处理。据说这是因为某些中文键盘的映射使得这种环境的用户很可能是想输入那个7位ASCII值的点号而不是这个全角句号。 6.服务器端口 🤟🦼↔🐯 服务器端口部分是可选的,通常在服务器连接的网络端口并非标准端口时才会用到。基本上浏览器支持的所有协议以及第三方应用都会以TCP或UDP作为传输方式,而TCP和UDP都会依赖于一个16位端口号来区分运行在一台机器上的不同服务(这意味着端口号数不能大于2的16次方,即65536)。服务器上每种协议通常都会关联一个默认的服务端口(如HTTP为80、FTP为21,等等),但这个默认值可以在URL里另行改写。 注意这个功能无意中造成了一个有趣的副作用,我们可以用浏览器向任意网络服务发送攻击者提供的数据,尽管浏览器并不支持这些服务的协议。例如,你可以把浏览器指向: 💪🌡🥑🆚🦠 而这里25端口实际上运行的是简单邮件传输协议(Simple Mail Transfer Protocol,SMTP)服务,并非HTTP协议。这个问题曾导致一系列的安全问题,浏览器也为此引入了一系列不太完美的解决方案。7.层级的文件路径 URL的下一个组成部分叫层级文件路径,它非常形象地体现了从服务器获取特定资源的方式,例如/documents/2016/my_diary.txt。规范里也公开表明,这个格式是直接从UNIX目录语义借用过来的,所以也支持在路径里出现的“/../”和“/./’,而对非绝对路径形式的URL,也会根据这种目录格式,加上基准路径再对应到其相对位置上去。 👩🧥📞😘🦴 在20世纪90年代的时候,Web服务器只是用来存放一堆静态文件和少量可执行脚本的简单的工作网关,所以使用文件系统模型是很自然的选择。但在那之后,很多当前的Web应用框架已经和文件系统没什么关联了,它们都是直接和数据库对象或常驻程序的注册位置打交道了。虽然还是有可能把这些数据结构和运行良好的URL路径对应起来,但人们往往不会这么做或者不会特别严谨地这么做。所有这些问题都会导致内容的自动索取、索引和安全测试要比预想的来得更复杂。#y448: 8.查询字符串 👎🚘🍓🆒🦋 查询字符串(Query String)也是个可选项,用于把一串非层级格式的任意参数传递给由前面路径所对应的资源。例如,以下例子就是一个把用户提供的信息传递给服务器端脚本,用于搜索的常见形式: http://example.com/search.php?query=Hello+world 大多数网站幵发者都很熟悉这种搜索字符串的特定格式;它是由浏览器在处理HTML文件里的表单时生成的,格式如下: 👎🌡🍇❌🐅 namel=valuel&name2=value2… 但让人意外的是,在URL的RFC文档里其实并没有硬性规定要使用这样的格式。实际上,规范里是把查询字符串当作一堆含混笼统的数据对待的,关键是接收者最后要怎么用它,所以有别于路径,它无需遵守特定的解析规则。 👨🎨🥼🦯🤤👆 关于我们上面提到的这种查询字符串的常见格式的说明,请参见信息量很庞大的RFC1630、与邮件协议相关的RFC2368以及HTML规范里与表单处理相关的内容。但这些规定全都不是强制性的,因此,如果某个Web应用真要完全不顾常规,把一堆乱七八糟的数据放在URL的查询字符串位置上,也并不能算错。 9.片段ID 片段ID(Fragment ID)的角色和查询字符串有点类似,但用法颇为晦涩,它是用于客户端而非服务器端(实际上,这个值根本就不应该传回给服务器端)的一种可选信息。在RFC里没有明确规定片段ID的格式或功能,但暗示性地提到可以用于在返回的文档里定位“子资源”或对文档渲染的方式提供一些帮助性的指导。👴👙🛏😷🤳 在实际运用中,片段ID在浏览器里只有一个用途:指向HTML页面里的某个锚点名称,用于页面浏览定位。这套逻辑很简单。如果在URL里的锚点名称与HTML页面里设定的锚点标签匹配,文档就会滚动到该定位标签的位置上,方便阅读浏览;否则,就啥动作也没有。由于片断ID的信息是包含在URL里的,所以能用它直接分享一篇长文里的具体位置或加入书签以便于保存。 由于片段ID只能用于在当前文档里定位,所以无需再从服务器端加载数据,只是根据用户的点击动作,稍许更新一下URL里的片段ID信息而已。 👌🚂🍪📶🐤 这种有趣的属性现在有了新用法,可以用这个值来存放一些临时的数据:如存储客户端脚本需要的各种状态信息。例如,地图浏览的应用可以把地图的当前坐标置于这个标识里,这样就可以把链接加到书签里或发送出去与他人共享,地图应用就能再回到相同的位置。和改变查询字符串的效果不同,片段ID不会触发页面重载,也就不需要时间开销,这使得它成为数据存储上的杀手级特性。 10.把所有的东西整合起来 上述提到的各个URL部分都有一些特定的保留字符:如正斜杠、冒号、问号等。为了确保浏览器的正常工作,这些字符除了用于分隔URL的不同部分,在URL里不能有其他的用途。谨记这点,现在假设我们要设计一个示例算法,来模拟浏览器的解析方式,把绝对URL按各个功能项分割开来。一个较为合理的算法大致如下。 🖕🌞🍭🆎🐒 步骤1:提取协议名称。 扫描第一个“彳’字符。在该字符左边的URL部分就是协议名称。如果获得的协议名称中出现了不应有的字符,这可能就是个相对URL,获得的并不是协议名称。 步骤2:去除层级URL标记符。🧑🍳👖📏😡🤌 字符串“//”应该跟在协议名称的后面。如果发现有该字符串,则跳过该标记符;如果未找到,就不用管了。 注意:在某些运行环境的解析中,出于可用性的考虑URL标记符甚至可以完全不用斜杠、只用1个斜杠、用三个甚至三个以上的斜杠。因着同样的思路,可能是为了帮助那些不熟悉的使用者吧,IE浏览器从一开始就接受在URL任意位置使用反斜杠(\)替代正斜杠。除Firefox以外,所有的浏览器也逐渐遵从这一规律,会接受像这样的URLhttp:\\example.com\。 步骤3::获取授权信息部分。 👨🦱👑🖥🤪👂 依次扫描或“#”符号,哪个先出现以哪个为准进行截取,从URL里提取出来的部分就是授权部分信息。刚才也提过了,大多数浏览器还接受反斜杠“\”作为正斜杠形式分隔符的替换写法,也要考虑这种情况。除了IE和Safari浏览器之外,分号;也是授权信息部分可接受的分隔符;这么做的原因还未可知。 步骤3A:定位登录信息,如果有的话。 授权部分信息提取出来后,在截取出来的信息里再寻找@符号。如果找到了,那在它前面的部分就是登录信息,这部分登录信息还需要做进一步分解,在第一个冒号前面(如果有的话)是用户名,后面则是密码数据。 🤟🗼🍭♀🐞 步骤3B:提取目标地址。 授权信息部分剩下的就是目标地址了。第一个冒号分隔开的就是主机名和端口号。用方括号括起来的是IPv6地址,这也是特例。 步骤4:确定路径(如果的确存在)。 如果授权部分的结尾跟着一个正斜杠——或者某些场景里,跟着一个反斜杠或分号,就像之前提到的,依次扫描下一个“?”、“#”或字符串结尾符,哪个先出现以哪个为准。截取出来的就是路径部分,当然最后应根据UNIX路径语义进行规范化整理。 🤙🗺🍞🅰🐅 步骤5:提取查询字符串(如果的确存在)。 . 如果在上一条解析里,后面跟的是一个问号,则继续扫描下一个“#”或到字符串结尾,哪个先出现以哪个为准。这中间的部分就是查询字符串。 步骤6:提取片段ID(如果的确存在)。 👂🗼🫖🆒🦌 如果成功解析完上一条信息,它的最后跟着“#”符号,从这个符号到整个字符串的最后,就是片段ID。换而言之,整个任务完成啦! 这个算法看起来很普通,但它揭示了一些甚至资深程序员也往往忽略掉的微妙细节。它也展示了对普通用户来说,要分辨一个URL会被怎么解析其实非常困难。让我们以这个相当简单的例子展开讨论吧: &gibberish=1234@167772161/👨🚒🩰🖌😃👄 这个URL的目标地址——实际上就是那串经过编码的数字,解码翻译过来其实是10.0.0.1(十进制的167772161转成十六进制则为A000001,所以实际对应的是:0A.00.00.01,即10.0.0.1)——如果不是专家,实在不容易把它辨认出来,很多用户可能会认为他们是在访问example.com呢 好吧,也许这个还算简单!让我们再看一个: 🧑⚕️👑🪝😒🤝 \@coredump.cx/ 在Firefox里,这个URL会把用户带往coredump.cx,因为example.com\会被认为是个合法的登录信息。而在其他的浏览器里,会被认为是个路径分隔符,所以用户最终会访问example.com IE里一个更令人抓狂的例子如下: 🥷🧢📬😪🙏 ;.coredump.cx/ 微软浏览器允许在主机名称里出现“;”符号,并成功解析到了这个地址,当然这得需要coredump.cx提前做了这样的域名解析设置。大多数其他浏览器会自动把URL纠正成: 然后把用户带到example.com(Safari除外,它会认为这个写法有语法错误)。👔🏮😡💅 如果你觉得局面已经一片混乱了,请记住,我们这才刚刚幵始讲浏览器是怎么工作的! ![]() 下面的 3、4、5楼继续本课。 WEB安全第五课第一节: 👩👑🛒😴
帖子热度 1.6万 ℃
|
|
对URL的分析之一 保留字符和百分号编码
上一节的URL解析算法里有个大前提:就是某些用于语义分隔的保留字符没有出现在URL里(也就是说用户名、请求路径等位置不会出现这些字符h这些会破坏语法的分隔符包括: :/?#[]@ 🤞🗺🍪🈴🐅 RFC还规定了若干的底层分隔符,尽管没有给出它们的详细用途,但这些符号是留给上层协议或具体的应用来实现的: !$&'()*+,;= 以上所有字符原则上都是被禁止的,但有时候也确实需要在URL里用到它们(譬如,要允许用户搜索任意字符串,并通过查询字符串传给服务器端)。既然不能禁用它们,该标准就提供了一种使用方法,即对这些有问题的字符进行编码。简单来说,这个方法就叫百分号编码或URL编码,它以一个百分号(%)和该字符的ASCII编码所对应的2位十六进制数字去替换这些有问题的字符。 👄🗽🍒✡🐞 例如,“/”会被编码成 %2F (—般用大写,但不强制)。为避免混淆,百分号本身被编码成 %25 。中间程序在处理已存在的URL(如在浏览器或Web应用里涉及的URL)时,绝对不要对这些待中转的URL中的保留字符再进行编码或解码,以免无意中改变了这些URL的原本含义。 但很遗憾,在碰到那些错误使用了这些保留字符,技术上来说并不合法的URL时,浏览器会出各种状况,因此对URL保留字符的处理也并非完全一成不变的。由于规范里并没有说到这些情况要怎么处理,导致浏览器开发商们完全是自由发挥,各家的实现并不一致。 👩🩰🛒🤔 例如,这个URL: http://a@b@c/ 到底应该被编码成: 🤌🚤🎂‼🐴 http://a@b%40c/ 还是: 👈🚂🦞✔🕊 http://a%40b@c/ 呢? IE和Safari浏览器认为前者更合理;而其他浏览器则选择支持后一种做法。 不在保留字符集里的字符理应不会对URL语法有什么特别重大的影响。然而,有一些字符(如非打印ASCII控制字符)明显会影响到URL的可读性和传输安全。因此RFC规定了一个名字很让人摸不着头脑的所谓非保留字符子集(包括字母数字、"-"、"."、“_”和“〜”)并说只有这个子集里的字符和位于正确功能位置上的保留字符才允许出现在URL里。🧑🚀🛍🪝☠✍ 注意令人奇怪的是,虽然规定里只允许非保留字符才能以未经编码的形式出现在URL里,但却未禁止非保留字符以编码形式出现。所以客户端软件可以随意地对它们编码或解码,编解码后URL的含义其实并没有变化。这个特点带来了另一个让人非常困惑的现象:在URL里可以用非正规的方式表示非保留字符。以下几个例子含义实际上完全一样: http://%65xample.%63om/ 🖕🌧🥣🆘🦕 http://%65%78%61%6d%70%6c%65%2e%63%6f%6d/e 另外还有一些可打印字符原本也应该属于非保留字符之列,却被排除在非保留字符集之外了。因此严格来说,根据RFC的规定它们应该被无条件编码。然而,浏览器有时候也并非完全按规则办事的,它们对这条规定并没有很当真。特别是,所有的浏览器都允许在URL里出现"^"、"{"、"|"和"}"而且无需被编码,就会直接把这些字符发给服务器端。 IE浏览器更是允许"<"、">"和"`",IE、FireFox和Chrome全都接受“\”;Chrome和IE还允许双引号,而Opera和IE都允许直接传送非打印字符0x7F(DEL)。👦🪓🤡✌ 最后,和RFC里的明确规定不同,绝大部分浏览器对片段ID都不会进行编码。这对依赖于这个字串的客户端脚本会产生难以预期的问题,只能指望一些会带来潜在风险的字符最好永远都不要出现。 对非US-ASCII文本字符的处理 🧒🎒😉🤝 全球有许多语种都超出了基础7位ASCII字符集甚至所有PC兼容系统默认使用的8位代码页(也即CP437)。甚至有些语言压根就不是使用拉丁体系字母表的。 所以在Web出现之前,就产生了为兼容这些常被忽视但有广泛使用基础的非英语用户的各种8位代码页(8 bit code page),使用的是各种高位字符集:ISO 8859-1、CP 850和用于西欧语言体系的Windows 1252;用于东欧和中欧体系的ISO 8859-2、CP 852和Windows 1250;用于俄语的KOI8-R和Windows 1251。由于某些语言的单词远非256个字符空间能覆盖的,就产生了一些更复杂的宽字节编码方式,如用于日文片假名的Shift JIS。 不兼容的编码字符集令使用不同代码页的计算机之间交换文件变得很困难。到20世纪90年代初,这个问题日益严重,因此诞生了Unicode——这是一种统一的通用编码字符集,但它太大了,8位编码长度是不可能覆盖全部区域用到的各种字符和形形色色的象形文字的。Unicode的改进版本是UTF-8,一种涵盖所有字符但相对简单的可变宽度编码方式,理论上来说,UTF-8对传统8位格式编码的应用是安全的。但UTF-8比起其他编码方式,需要用更多字节去编码高位字符,很多用户觉得这有点浪费且没必要。因为这些批评,使得UTF-8编码方式在出现10年之后才逐渐在Web上获得认可,而此时相关的Web协议早已尘埃落地固定成型了。🧑💻👠🪟💩🤌 很遗憾,这种滞后对URL里的用户输入数据有一定的影响。浏览器需要在很早期就兼容这种用法,但开发人员在相关的协议里寻求帮助时,他们找不到任何有意义的建议。甚至在多年以后的2005年,RFC3986也仅是这么说: 有了新的技术手段之后,用户在本地或区域性语言相关的上下文里,允许使用更宽范围内的字符;但本规范对具体怎么使用这些技术未作规定。 🧒🧥🛒😈🖕 在URI里,……如出现超过该URL相应规范和协议元素限定范围内的字符,就必须用8位元数据的百分号编码形式来表示这些超出US-ASCII码表之外的字符。但把URI里这些字符编码成百分号形式之前,需要先设定字符的编码方式。 唉,虽然心怀美好的愿望,但这些标准就再未对此问题有进一步描述了。所以尽管的确能在URL里传递原始的高位字符,但如果不知道它们是按哪种代码页进行解析的,服务器端就不知道传过来的一个%B1究竟代表了"±"还是"a"或是某个乱码字符,这完全取决于用户的使用环境了。 令人痛心的是,浏览器开发商们在这个问题上非常消极被动,并没有想出一个持久统一的解决方案。大多数浏览器都会先把URL里的路径信息部分,进行内部转码,变成UTF-8格式(甚至如果可能,就只按ISO 8859-1格式解析),而查询字符串部分则按照当前页面的编码方式进行转换。在某些情况下,对于手工输人的URL或发给特定API处理的URL,高位字符就会被自动降级成7位US-ASCII码所对应的字符,结果产生一堆的问号形式乱码,甚至由于具体实现上的漏洞,出现完全混乱的结果。👩🧥🩺😚🤞 不管浏览器上的具体实现效果如何,总之在查询字符串和路径里传递非英语字符的问题都是一项大大的烦扰。但URL里服务器地址的编码却没有碰到这种百分号编码的问题:因为目标服务器的名称里不能包含高位字符,至少理论上符合规范的DNS域名只允许出现字母数字和横线,然后用句点作为分隔符号——如果有人不遵守这条规则,出错的具体信息取决于每台域名服务器的返回了,并没有成规。 聪明的读者或许要问:为什么会提到这个限制呢?换而言之;真的需要用到非拉丁字母组成的本地化域名吗?要回答这个问题还真有点棘手。其实很简单,从前有些群众认为如果不支持这类编码方式,会降低全球商业用户和个人用户对Web的接受度与使用范围——然后,也不管是否的确如此,他们决定支持本地化域名。 🖕🏦🍌♊🐮 这番努力的结果就是诞生了IDNA(Internationalized Domain Namesin Applications,国际化域名)。首先RFC3490描述了一种使用字母数字和横线,很别扭地对任意Unicode字符串进行编码的方式,然后RFC3492描述了把这种编码应用在DNS域名上的做法,这就是Punycode格式。Punycode的样子大致如下: xn——[US-ASCII部分]-[经过编码的Unicode数据] 在兼容这套机制的浏览器的地址栏里,如果输入的主机名中包含了非US-ASCII字符,浏览器会先把这种非法字符转换成Punycode,然后再进行DNS解析。同样地,如果在浏览器的地址栏里填入包含Punycode的URL,浏览器应该以解码后的、我们可辨认的形式显示该URL。 🤝🚈🍚⚛🦟 注意如果把这些各不兼容的编码策略都组合在一起,能获得极富娱乐效果的混搭。下面我们就以这个虚构的毛巾商店为例来看看: ![]() 在各种和URL编码有关的处理里,IDNA很快就变成最有问题的一个。因为从根本上来说,显示在浏览器地址栏里的URL域名部分是Web最重要的安全标记,它能让用户迅速区分出他们信任的并且打过交道的网站和其他陌生的互联网站点。当显示在浏览器里的域名是波兰人民非常熟悉易认的那38个的字母时(这里的38个字母的说法,是针对作者的母语波兰语而言的),只有特别粗心的用户才会弄错真域名和假域名。但IDNA不加区分地把这38个字符扩展成支持10多万个象形字符的Unicode之后,这些字符再经过编码后就相差无几了,彼此仅有实际功能上的差异而已。#362: 🤝🛑🍧©🦠 事情会有多糟糕?我们以斯拉夫语字母为例,其中一些字母颇有些长得和拉丁语言体系里的一样,但其Unicode编码值完全不同,解析后会对应完全不同的Punycode域名: ![]() 在IDNA提出这套方案和浏览器刚开始支持这套机制的时候,没人觉得这有什么问题。浏览器开发商们明显假定DNS注册机构方面会确保不会有人注册两个很形似的域名,而域名机构却认为该由浏览器开发商负责解决地址栏里非常容易引起误解的视觉效果问题。👳💎🦯👻👏 到2002年,与之有关的各方终于意识到问题的严重性。这一年,Evgeniy Gabrilovich和Alex Gontmakher发表了“The Homograph Attack”11,在该论文里探讨了这种漏洞的细节。 他们指出,即使在技术上能实现,但任何由注册机构方所做的防范措施,都是有漏洞的。攻击者只需要购买一个正常的顶级域名,然后在自己的域名服务器里设置一条二级域名记录,再经过IDNA的转换之后,看上去就和example.com/完全一模一样(实际上最后的斜杠字符并非功能性的分隔符号,而是一个仅在外观上完全相同的字符而已)。#367: 🕶🧬😇👃 ![]() 所以域名注册机构干不了什么事情,主动权还是在浏览器开发商手上。但他们究竟能做什么呢?实际上,开发商们能做的事情也并不多。 我们现在意识到,实在很难找到一味既简单又无痛的药方来解决那个非常欠缺远见的IDNA标准。浏览器开发商们应对这一风险的做法是,如果用户的本地语言和该域名网页里所用的语种不相符,则浏览器里仍以难懂的Punycode方式来显示域名(这导致在浏览他国网站,或使用一台进口的电脑,又或仅仅是电脑的语言配置不对时,会发生浏览出错);只能在国家相关的顶级域名里,使用IDNA方式的域名(这样在.com域名和其他较为常用TLD里就不能使用国际化的域名了);或者把那些外观样貌与斜杠、句号、空格符这类字符看起来很相似的“坏”字符放到黑名单里(其实这是徒劳无益的,世上这类字型何其多)。 👴👗⚔😷🧠 这些做法极大地阻碍了国际化域名的应用,尽管这个标准现在也只是在苟延残喘,但百足之虫僵而不死,而它为非英语国家的用户提供的帮助其实远比不上它所带来的安全问题。 |
对URL的分析之二 常见的URL协议及功能
让我们把光怪陆离的URL解析先放一边,再次回归本源吧。在早些时候,我们就提到过,某些协议可能会带来难以预期的安全问题,因此,任何与用户提交的URL打交道的Web应用都务必要谨慎小心。为了更清楚地解释这点,很有必要先回顾一下典型的浏览器环境里常见的URL协议,基本上可以将其划分为四种类别。 🧑⚕️🎩🖌😫👂 1.浏览器本身支持、与获取文档相关的协议 这些协议由浏览器内部直接处理,通过特定的传输协议,获得指定文档的内容,并通过常规的内核解析引擎的逻辑处理,把文档最终呈现出来。这也是URL最基本和最常用的功能了。 ✌🌡🔪🈳🐙 这个最常见类别里的协议却出人意料地很少:http:(RFC2616)它是Web领域里最主要的传输模式,https:(RFC281812)加密版的http;还有ftp:—个历史相对久远的传输协议(RFC95913)。而所有的浏览器都支持file:协议(以前也叫local:),它用于访问本地文件系统或NFS和SMB共享(但file:协议通常无法从互联网上的网页访问到)。 还有两个不太为人所知的协议也应该简要地说两句:浏览器内置支持的gopher协议,它是一种被淘汰的早期Web原型(RFC143614),但Firefox仍然支持这一协议;而IE浏览器里则仍然支持shttp:这是一个失败的https尝试(RFC266015),但现在它仅仅是作为HTTP的一个别名而已。 2.由第三方应用和插件支持的协议 🖕🌦🍼🈸🐒 浏览器碰到这类协议,会根据URL的匹配情况,把具体的处理转给外部的某个应用程序,就可以实现类似媒体播放、文档浏览或IP电话等功能。到这一阶段,就已经无需浏览器的参与了(大多数情况下如此)。 到今时今日,已经有几十个这类协议处理程序了,要把它们都说清楚只怕还得再写一本厚厚的书。一些最常见的协议包括:acrobat:,看字面就知道是用于触发AdobeAcrobat的pdf阅读器;callto:和sip:则用于各种即时通信工具和电话软件;daap:、itpc:和itms:用于苹果公司的iTunes软件;mailto:、news:和nntp:用于邮件和Usenet新闻网客户端软件;mmst:、mmsu:、msbd:和rtsp:是流媒体播放器专用的,反正诸如此类还有很多。 🏫🥚☪🐒 浏览器自身有时候也在这个列表里。譬如之前提到的firefoxurl:协议就会在另一个浏览器里启动Firefox程序;而cf:协议则可以在IE里调用Chrome。 大多数情况下,通常来说URL里这些协议对发起它们的Web应用自身的安全性并无影响(但这不是绝对的,特别是在一些需要插件支持的内容里)。值得注意的是,这些第三方协议的处理程序往往漏洞百出,并有可能导致操作系统被入侵。因此,一个可靠的网站应该尽量避免使用那些来历不明的协议。 3.未封装的伪协议👴👖📱🥰🙌 有一系列内部保留协议是用于方便访问浏览器的脚本解析引擎和某些内部功能的,它们不需要真的去远端获取数据,甚至也不需要创建一个独立的文档上下文环境来展示运行的结果。这种伪协议大多和浏览器紧密相关,一般无法通过互联网访问到,通常也不能干什么大的坏事。然而,这条规律有几个重要的例外。 也许最广为人知的例外就是javascript:协议了(在更早期的时候,它还有诸如livescript:或mocha:这样的别名,后者见于Netscape浏览器中)。这个协议可以在当前浏览器访问的网页环境里,调用JavaScript编程语言的解析引擎Q在见浏览器里,vbscript:协议也有类似的功能,只不过用的是微软专有的Visual Basic接口。 🙌🌦🍪❎🦚 另一个重要的例子是data:协议(RFC239716)它不需要任何额外的网络请求,就能创建一个短小的内置式文档(inline document),有时候它可以通过继承而获得发起该URL的那个页面的某些操作权限。一个data:协议的例子,其URL如下: data:text/plain,Mhy,%20hello%20there! 这些能从外部访问到的伪URL对网站的安全有严重影响。一旦被访问到,其所包含的攻击数据(payload)就可以用发起端服务器运行环境的权限,获得执行,最后偷取受影响用户的敏感信息或改变页面的显示内容。🧓👓📐😡👃 4.封装过的伪协议 这种特别的伪协议可以放在任意URL之前,它指示将取回的内容强制进行特殊的解码或者值染显示。这一类别里最广为人知的例子就是Firefox和Chrome支持的view-source:协议了,它会按照整齐的排版格式显示HTML页面的源代码。这个协议的用法如下: 👵🪖💳💩👄 ![]() 其他的类似协议包括Jar:,Firefox可以通过这个协议解压这种类似ZIP的文件内容;wyciwyg:和view-cache:分别为Firefox和Chrome浏览器访问缓存的方式;oddballfeed:协议用于在Safari里获得新闻推送信息;另外还有一堆与Windows帮助系统和其他Windows组件有关的协议,这类协议的支持文档很差也鲜有资料可查(hep:、its:、mhtml:、mk:、ms-help:、ms-its:和ms—itss:) ✋🛩🫑♑🪰 许多封装型协议的共同特点是,攻击者通过这种方式,隐藏最后实际是由浏览器处理的真实URL地址,如view-source:javascript:(或甚至写成这样:view-source:view-sourcejavascript:),后面再跟一段恶意代码,就能达到目的。可能会有一些安全限制以避免这种漏洞,但通常这类限制都不太能指靠得上。另一个重要的问题是,尤其是在微软的mhtml:协议里,它可能会忽略服务器返回的HTTP内容设置指令,而产生广泛的影响。 5.关于协议检测部分的结语 各种伪协议的数量极多,这也是Web应用必须严谨地鉴别用户提供的URL到底是什么协议的原因。通常对URL的解析模式往往既不靠谱,每种浏览器的处理也不同,再加上浏览器支持的许多协议都有很强的开放性,仅仅把已知的危险协议放进黑名单里并不足够;例如,要检查协议名称是否为javascript:其实有各种规避的方法,例如在这个关键字的中间插入一个制表符或一个换行符,或代之以vbscript:,甚至可以在前面再加上另一层封装形式的协议。 👍🛑🌶🅰🦮 |
对URL的分析之三 相对URL的解析
在之前的篇幅里曾多次提到过相对URL,它仍然值得我们再额外地说一下。相对URL之所以存在,是因为互联网上的每个网页,几乎都不可避免地会引用许多与它同一服务器的其他URL地址,甚至多半就与它处于同一个目录。所以,如果这个文件里的每次引用,都要使用完整的URL地址,就会非常不方便并且很浪费资源,因此就会用到这种较短的相对URL表达方式(如../other_file.txt)。相对URL里缺失的部分,就由发起引用的那个URL自身的信息补齐。 👈🎢🍏®🪰 因为相对URL的使用场景和绝对URL没什么差别,所以浏览器就必须有办法识别出它们的差别。这样对Web应用也有好处,因为大多数的URL过滤器都只需要对绝对URL进行清理,而本地的相对URL则可以保持原样。 按照规范里的说法,要区分开两者非常简单:如果URL字符串不是以一个有效的协议名开始的,后面没有跟着冒号,又或者没有那个有效的“//”分隔符,那它就是一个需要被引用的相对URL。如果在解析这个相对URL时没有上下文环境,那就拒绝访问。如果有上下文运行环境,那它就必定是个安全的相对链接了,对吧? 👍⛴🔪❓🦟 很容易想象得到,这不像看上去那么简单轻巧。首先,如上一节里提到过的那样,因为各种浏览器的具体实现千差万别,有效协议名称的字符集也各自不同,然后还有各种替代“//”分隔符的做法。也许最有意思也很常见的一个错误观念,就是认为相对链接只能指向同一个服务器上的资源,但实际上,还是颇有几种不太明显的URL变型可以利用的。让我们看看相对URL有哪些类型,以便更进一步阐述各种可能性。 1.有协议名称,但没有授权信息(http:foo.txt)。这个臭名昭著的漏洞是在RFC3986里埋下的,这要拜更早期规范里的疏忽所赐。这些规范在描述里认为这种URL是(无效的)绝对地址,但所提供的解析算法却又含混地把这种地址的解析给搞错了。 在后来的地址解析算法里,对于这种形式的URL,它的协议、路径、查询字符串或片段ID都以这个URL自身为准,但授权信息的部分,以引用它的那个页面地址为准。许多浏览器都按照这个标准执行,但又执行得并非很统一。譬如,在某些情况下,http:foo.txt会被当作一个相对地址,而https:example.com却会被解析成绝对地址! ✊🛑🍼💲🐞 2.没有协议名,但有授权信息(//example.com)。这又是另一种臭名昭著令人困惑的写法,但对怎么处理这种格式的URL规范里倒是写得比较完整。尽管原本对于/example.com这种写法,浏览器会指向当前服务器的本地资源,对于//example,com这种写法,浏览器依据规范却做了完全不同的处理:保持当前页面的协议不变,然后把这串字符作为一个新的授权信息段。在这种情况下,协议名称由原发起页面确定,而所有接下来的URL信息都取自这个相对URL,构成完整的URL。 3.没有协议名、没有授权信息,但有路径(../notes.txt)。这是相对链接里最常见的一种形态了。协议和授权信息都从引用URL里复制过来。如果相对URL的开头不是斜杠,则相对路径会拼接在引用URL最右边的“/”后面。例如,如果基础URL为: 👨🚒👙😍🦷 则路径不变,相对路径直接跟在其后; 但如果初始路径是: 🧑💻🪥😇🖕 则需要把文件名的部分砍掉,再把相对路径接上去。新的路径加在原路径后,拼接起来的路径名再经过标准化的路径规范整理。而查询字符串和片段ID都只来自相对URL。 4.没有协议名、没有授权信息、没有路径,但有查询字符串(?search=bunnies)。这种情况下,协议、授权信息、路径信息全都原封不动地从原引用URL复制过来。查询字符串和片段ID则来自相对URL。 👨🚒👠🦯🥰👌 5.只有片段ID(#bunnies)除片段ID之外,所有信息全都原封不动地从原引用URL复制过来;只替换片段ID的部分。如之前章节讨论过的那样,点击这类相对URL通常不会导致页面的重新加载。 因为应用级别的URL过滤和浏览器本身的可能会有差异,在处理这种相对地址时,不直接输出任何由用户提供的相对URL是靠谱的做法。如果可行,相对地址应该直接改写成绝对地址,而所有的安全检查应针对经过以上调整后符合规范的URL地址。 小执念遇到高手虚心请教,获得 2 个 金币.
|
说得好啊!我在XX社区打滚这么多年,所谓阅人无数,就算没有见过猪走路,也总明白猪肉是啥味道的。一看到楼主的气势,我就觉得楼主同在社区里灌水的那帮小混混有着本质的差别!那忧郁的语调,那熟悉的签名,还有字里行间高屋建瓴的辞藻。没用的,楼主,就算你怎么换马甲都是没有用的,你的亿万拥戴者早已经把你认出来了,你一定就是传说中的最强ID。自从社区改版之后,我就已经心灰意冷,对社区也没抱什么希望了,传说已经幻灭,神话已经终结,留在社区还有什么意思?没想到,没想到,今天可以再睹楼主的风范,我激动得忍不住就在屏幕前流下了眼泪。是啊,只要在楼主的带领下,社区就有希望了。我的内心再一次沸腾了,我胸腔里的血再一次燃烧了。楼主的几句话虽然简单,却概括扼要,一语道出了我们苦想多年仍不可解的几个重大问题的根本。楼主就好比社区的明灯,楼主就好比社区的方向,楼主就好比社区的栋梁。有楼主在,社区的明天必将更好!
|
一个老师问三个学生,你们用什么东西可以将一间屋子填满。第一个学生找来了稻草,铺满了地板,老师摇了摇头。第二个学生找来一根蜡烛,顿时屋子里充满了光芒,老师还是摇了摇头,因为学生的影子没有被照到。 这时第三个学生往地板上丢了块肥皂,没一会,欢快的娇喘声便充满了整个房间。#390:
|
丑的人还在沉睡~~~
👂🚠🥑☪🦬 ⊂⌒/ヽ-、__ /⊂_/____ /  ̄ ̄ ̄ ̄ ̄ ̄ ̄ 帅的人已经醒来看帖~~~ ∩∩ ✊🛩🦀↔🐯 (´・ω・) _| ⊃/(___ / └-(____/  ̄ ̄ ̄ ̄ ̄ ̄ ̄ |