一个必须防止跨站恶意内容的特殊情况是,Web应用程序被设计为接受来自一个用户的HTML或XHTML,然后将其发送给其他用户(有关跨站恶意内容的更多信息,请参阅第7.15节)。以下小节讨论过滤这种特定类型的输入,因为处理它是如此常见的需求。
最安全的方法是移除所有可能的(X)HTML标签,这样它们就不会影响任何东西,而且这相对容易做到。如上所述,您应该已经识别了合法字符的列表,并拒绝或移除列表中不存在的字符。在这个过滤器中,只需不要将以下字符包含在合法字符列表中:``<'',``>''和``&''(如果它们在属性中使用,则包括双引号字符``"''')。如果浏览器仅根据HTML规范运行,则无需移除``>'',但在实践中必须移除。这是因为某些浏览器假定页面作者真的想输入一个开头的“<”,并“乐于助人”地插入一个 - 攻击者可以利用这种行为并使用“>”来创建不希望出现的“<”。
通常,传输HTML的字符集是ISO-8859-1(即使发送国际文本也是如此),因此过滤器还应省略大多数控制字符(换行符和制表符通常可以接受)以及高位已设置的字符。
这种方法的一个问题是,它可能会真正让用户感到惊讶,特别是那些输入国际文本的用户,如果所有国际文本都被悄悄移除。如果无效字符被悄悄移除而没有警告,则数据将不可挽回地丢失,并且以后无法重建。一种替代方法是禁止此类字符,并将错误消息发送回尝试使用它们的用户。这至少会警告用户,但不会给他们提供他们正在寻找的功能。其他替代方法是编码此数据或验证此数据,这将在接下来讨论。
一种几乎同样安全的替代方法是转换关键字符,使其在HTML中不会具有通常的含义。这可以通过将所有“<”转换为“<”,“>”转换为“>”,以及“&”转换为“&”来完成。任意国际字符可以使用格式“&#value;”以Latin-1编码 - 不要忘记结尾的分号。对国际字符进行编码意味着您必须知道输入编码是什么,当然。
这里可能存在的一个危险是,如果这些编码被意外地解释两次,它们将成为漏洞。但是,这种方法至少允许后来的用户看到输入的“意图”。
某些应用程序为了正常工作,必须接受来自第三方的HTML并将其发送给他们的用户。请注意 - 此时您正在踏入危险地带;请确保您真的想这样做。即使是接受来自任意位置的HTML的想法在一些安全从业人员中也存在争议,因为它极其难以正确处理。
但是,如果您的应用程序必须接受HTML,并且您认为值得冒险,则至少要识别一个“安全”HTML命令列表,并且只允许这些命令。
这是一个最小的安全HTML标签集,可能对支持简短评论的应用程序(例如留言簿)有用:<p>(段落),<b>(粗体),<i>(斜体),<em>(强调),<strong>(强烈强调),<pre>(预格式化文本),<br>(强制换行 - 请注意它不需要结束标签),以及它们的所有结束标签。
您不仅需要确保只接受一小部分“安全”HTML命令,还需要确保它们正确嵌套和关闭(即,HTML命令是“平衡的”)。在XML中,这被称为“格式良好”的数据。如果您接受标准HTML,则可以进行一些例外处理(例如,支持在<p>之前未提供的隐含的</p>是可以的),但是尝试接受其完全通用性中的HTML(在许多情况下可以推断平衡的结束标签)对于大多数应用程序来说是不需要的。实际上,如果您试图坚持使用XHTML(而不是HTML),那么格式良好是必需的。此外,HTML标签是不区分大小写的;标签可以是全部大写,全部小写或混合大小写。但是,如果您打算接受XHTML,则需要要求所有标签都为小写(XML是区分大小写的;XHTML使用XML并要求标签为小写)。
以下是一些关于如何执行此操作的随机提示。通常,您应该设计围绕HTML文本和允许标签集的任何内容,以便贡献的文本不会被误解为来自“主”站点的文本(以防止伪造)。除非您已检查属性类型及其值,否则不要接受任何属性;有许多属性支持诸如Javascript之类的东西,可能会给您的用户带来麻烦。您会注意到,在上面的列表中,我根本没有包含任何属性,这当然是最安全的做法。如果使用了不安全的标签,您应该发出警告消息,但如果这不切实际,则编码关键字符(例如,“<”变为“<”)可以防止数据丢失,同时保持用户安全。
在扩展此集合时要小心,通常要限制您接受的内容。如果您的模式过于宽松,则浏览器可能会以与您期望不同的方式解释序列,从而导致潜在的漏洞。例如,FozZy在Bugtraq(2002年4月1日)上发布了一些序列,这些序列允许在各种基于Web的邮件系统中进行利用,这可能会让您了解您需要防御的各种问题。这是一些漏洞利用文本,曾经可以颠覆Microsoft Hotmail中的用户帐户
<SCRIPT> </COMMENT> <!-- --> --> |
<_a<script> <<script> (Note: this was found by BugSan) |
<b onmousover="...">go here</b> <img [line_break] src="javascript:alert(document.location)"> |
<a href="javascript#[code]"> <div onmouseover="[code]"> <img src="javascript:[code]"> <img dynsrc="javascript:[code]"> [IE] <input type="image" dynsrc="javascript:[code]"> [IE] <bgsound src="javascript:[code]"> [IE] &<script>[code]</script> &{[code]}; [N4] <img src=&{[code]};> [N4] <link rel="stylesheet" href="javascript:[code]"> <iframe src="vbscript:[code]"> [IE] <img src="mocha:[code]"> [N4] <img src="livescript:[code]"> [N4] <a href="about:<script>[code]</script>"> <meta http-equiv="refresh" content="0;url=javascript:[code]"> <body onload="[code]"> <div style="background-image: url(javascript:[code]);"> <div style="behaviour: url([link to code]);"> [IE] <div style="binding: url([link to code]);"> [Mozilla] <div style="width: expression([code]);"> [IE] <style type="text/javascript">[code]</style> [N4] <object classid="clsid:..." codebase="javascript:[code]"> [IE] <style><!--</style><script>[code]//--></script> <!-- -- --><script>[code]</script><!-- -- --> <<script>[code]</script> <img src="blah"onmouseover="[code]"> <img src="blah>" onmouseover="[code]"> <xml src="javascript:[code]"> <xml id="X"><a><b><script>[code]</script>;</b></a></xml> <div datafld="b" dataformatas="html" datasrc="#X"></div> [\xC0][\xBC]script>[code][\xC0][\xBC]/script> [UTF-8; IE, Opera] <![CDATA[<!--]] ><script>[code]//--></script> |
Konstantin Riabitsev发布了一些用于过滤HTML的PHP代码 (GPL);我没有仔细检查它,但您可能想看一下。
细心的读者会注意到,我没有将超文本链接标签<a>作为HTML中的安全标签包含在内。显然,您可以将<a href="safe URI">(超文本链接)添加到安全列表中(除非您已检查其内容,否则不允许任何其他属性)。如果您的应用程序需要它,请这样做。但是,允许第三方创建链接的安全性要低得多,因为定义“安全URI”[1]被证明非常困难。许多浏览器接受各种可能对用户有害的URI。本节讨论如何验证来自第三方的URI以重新呈现给其他人,包括合并到HTML中的URI。
首先,让我们简要地看一下URI语法(如各种规范所定义)。URI可以是“绝对”的或“相对”的。绝对URI的语法如下所示
scheme://authority[path][?query][#fragment] |
[username[:password]@]host[:portnumber] |
path[?query][#fragment] |
现在我们已经了解了URI的语法,让我们检查每个部分的风险
方案:许多方案都非常危险。允许某人在您的材料中插入“javascript”方案将使他们能够轻松发起拒绝服务攻击(例如,通过重复创建窗口,使用户的机器冻结或变得无法使用)。更严重的是,他们可能能够利用javascript实现中的已知漏洞。某些方案可能是令人讨厌的,例如在不期望邮件时使用“mailto:”,并且某些方案在客户端计算机上可能不够安全。因此,有必要将允许的方案集限制为少数几个安全方案。
授权:理想情况下,您应该将用户链接限制为“安全”站点,但这在实践中很难做到。但是,您当然可以对用户名,密码和端口号做一些事情:您应该禁止它们。期望用户名的系统(尤其是带有密码!)可能正在保护更重要的材料;这在公开发布的URI中很少需要,并且有人可能会尝试使用此功能来说服用户暴露他们有权访问的信息和/或使用它来修改信息。此类URI允许语义攻击;有关更多信息,请参阅第7.16节。没有密码的用户名同样危险,因为浏览器通常会缓存密码。您通常不应允许指定端口,因为不同的端口期望不同的协议,并且由此产生的“协议混淆”可能会产生漏洞。例如,在某些系统上,可以使用“gopher”方案并指定SMTP(电子邮件)端口来使用户发送攻击者选择的电子邮件。您可能允许一些特殊情况(例如,http端口8008和8080),但总的来说,这是不值得的。以名称指定的主机实际上具有相当有限的字符集(使用DNS标准)。从技术上讲,该标准不允许下划线(“_”)字符,但是Microsoft忽略了该标准的一部分,甚至在某些情况下要求使用下划线,因此您可能应该允许它。此外,在支持DNS名称中的国际字符方面已经做了大量工作,此处不再进一步讨论。
路径:允许路径通常是可以的,但不幸的是,某些应用程序使用路径的一部分作为查询数据,从而创建一个我们将接下来讨论的漏洞。此外,路径被允许包含诸如“..”之类的短语,这可能会在编写不良的Web服务器中暴露私人数据;这不像以前那么严重,并且应该由Web服务器修复。由于只有短语“..”是特殊的,因此查看路径(以及可能的查询数据)并禁止“../”作为内容是合理的。但是,如果您的验证器允许URL转义,则这可能会很困难;现在您需要防止其中一些字符被转义的版本,并且可能还必须处理这些字符的各种“非法”字符编码。
查询:查询格式(以“?”开头)可能存在安全风险,因为某些查询格式实际上会导致在服务终端发生操作。它们不应该这样做,您的应用程序也不应该这样做,有关更多信息,请参阅第5.12节。但是,我们必须承认现实是一个严重的问题。此外,许多网站实际上是“重定向器” - 它们采用一个参数来指定用户应重定向到的位置,并发送回将用户重定向到新位置的命令。如果攻击者引用此类站点并提供更危险的URI作为重定向值,并且浏览器轻易地服从重定向,则这可能是一个问题。同样,用户的浏览器应该更加小心,但并非所有用户浏览器都足够谨慎。此外,许多Web应用程序都存在漏洞,这些漏洞可以通过某些查询值来利用,但总的来说,这很难防止。官方URI规范不允许使用“+”(加号)字符,但在实践中,“+”字符通常表示空格字符。
片段:片段基本上定位文档的一部分;据我所知,只要语法合法,就没有基于片段的攻击,但是确实需要检查其语法的合法性。否则,攻击者可能能够插入诸如双引号(“)之类的字符,并过早地结束URI(破坏任何检查)。
URL转义:URL转义非常有用,因为它们可以表示任意8位字符;出于同样的原因,它们也可能非常危险。特别是,URL转义可以表示控制字符,许多编写不良的Web应用程序很容易受到这些字符的攻击。实际上,无论有无URL转义,许多Web应用程序都容易受到某些字符(例如反斜杠,&符号等)的攻击,但这很难概括。
相对URI:如果您管理好网站,则相对URI应该是相当安全的,尽管在某些应用程序中,也没有充分的理由允许它们。
这是我对“简单但大部分安全”URI模式的建议,该模式非常简单,可以通过“手动”或正则表达式来实现;允许以下模式
(http|ftp|https)://[-A-Za-z0-9._/]+ |
此模式不允许许多潜在的危险功能,例如查询,片段,端口或相对URI,并且仅允许少数几种方案。它阻止使用“%”字符,该字符用于URL转义,并且可以用于指定服务器可能未准备好处理的字符。由于它不允许使用“:”或URL转义,因此它不允许指定端口号,甚至使用它重定向到更危险的URI也很困难(由于缺少转义字符)。它还阻止使用许多其他字符;同样,许多设计不良的Web应用程序无法处理许多“意外”字符。
即使是这种“大部分安全”的URI也允许许多有问题的URI,例如子目录(通过“/”)和尝试向上移动目录(通过“..”);服务器应捕获此类非法查询。它允许一些非法的主机标识符(例如,“20.20”),尽管我不知道有任何情况会导致安全漏洞。某些Web应用程序将子目录视为查询数据(或更糟糕的是,作为命令数据);这很难普遍预防,因为找到“所有设计不良的Web应用程序”是绝望的。您可以阻止使用所有路径,但这将使引用大多数Internet信息成为不可能。该模式还允许引用本地服务器信息(通过诸如“http:///”,“http://localhost/”和“http://127.0.0.1”之类的模式)以及访问内部网络上的服务器;在这里,您将不得不依靠服务器正确地将生成的HTTP GET请求解释为仅是信息请求,而不是操作请求,如第5.12节中建议的那样。由于此模式不允许查询表单,因此在许多环境中,这应该足够了。
不幸的是,“大部分安全”模式也阻止了许多非常合法且有用的URI。例如,许多网站使用“?”字符来标识特定文档(例如,新闻网站上的文章)。“#”字符对于指定文档的特定部分很有用,并且允许相对URI在讨论中可能很方便。各种允许的字符和URL转义不包含在“大部分安全”模式中。例如,如果不允许URL转义,则很难访问许多非英语页面。如果您确实需要此类功能,则可以使用安全性较低的模式,意识到您在给用户带来更大功能的同时,也将用户暴露于更高的风险中。
一种允许查询,但至少限制了协议和端口的模式是以下模式,我将其称为“简单但有点安全模式”
(http|ftp|https)://[-A-Za-z0-9._]+(\/([A-Za-z0-9\-\_\.\!\~\*\'\(\)\%\?]+))*/? |
创建一个“有点安全”模式,真正将URI限制为合法值非常困难。这是我当前尝试这样做的方式,我称之为“复杂但有点安全模式”,以忽略空格并用“#”引入注释的形式表示
( ( # Handle http, https, and relative URIs: ((https?://([A-Za-z0-9][A-Za-z0-9\-]*(\.[A-Za-z0-9][A-Za-z0-9\-]*)*\.?))| ([A-Za-z0-9\-\_\.\!\~\*\'\(\)]|(%[2-9A-Fa-f][0-9a-fA-F]))+)? ((/([A-Za-z0-9\-\_\.\!\~\*\'\(\)]|(%[2-9A-Fa-f][0-9a-fA-F]))+)*/?) # path (\?( # query: (([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+= ([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+ (\&([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+= ([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+)*) | (([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+ # isindex ) ))? (\#([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+)? # fragment )| # Handle ftp: (ftp://([A-Za-z0-9][A-Za-z0-9\-]*(\.[A-Za-z0-9][A-Za-z0-9\-]*)*\.?) ((/([A-Za-z0-9\-\_\.\!\~\*\'\(\)]|(%[2-9A-Fa-f][0-9a-fA-F]))+)*/?) # path (\#([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+)? # fragment ) ) |
即使是上面显示的复杂模式也不能禁止所有非法URI。例如,再次,“20.20”不是合法的域名,但该模式允许它;但是,据我所知,这不应引起任何安全问题。复杂的模式禁止URL转义,这些转义表示控制字符(例如,%00到$1F) - 最小允许的转义值是%20(ASCII空格)。禁止控制字符可以避免一些麻烦,但这也具有局限性;如果您需要支持将所有控制字符发送到任意Web应用程序,请将“2-9”更改为所有位置的“0-9”。此模式确实允许路径中的所有其他URL转义值,这对于国际字符很有用,但可能会给一些无法处理它的系统带来麻烦。该模式至少可以防止空格,换行符,双引号和其他危险字符出现在URI中,从而防止在将URI合并到生成的文档中时发生的其他类型的攻击。请注意,该模式在许多位置允许使用“+”,因为在实践中,加号通常用于替换查询和片段中的空格字符。
不幸的是,如上所述,存在可以通过任何允许查询数据的技术起作用的攻击,并且一旦您允许查询,似乎就没有真正好的防御措施。因此,您可以从上面的模式中删除使用查询数据的功能,但允许其他形式,从而产生“复杂但大部分安全”模式
( ( # Handle http, https, and relative URIs: ((https?://([A-Za-z0-9][A-Za-z0-9\-]*(\.[A-Za-z0-9][A-Za-z0-9\-]*)*\.?))| ([A-Za-z0-9\-\_\.\!\~\*\'\(\)]|(%[2-9A-Fa-f][0-9a-fA-F]))+)? ((/([A-Za-z0-9\-\_\.\!\~\*\'\(\)]|(%[2-9A-Fa-f][0-9a-fA-F]))+)*/?) # path (\#([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+)? # fragment )| # Handle ftp: (ftp://([A-Za-z0-9][A-Za-z0-9\-]*(\.[A-Za-z0-9][A-Za-z0-9\-]*)*\.?) ((/([A-Za-z0-9\-\_\.\!\~\*\'\(\)]|(%[2-9A-Fa-f][0-9a-fA-F]))+)*/?) # path (\#([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+)? # fragment ) ) |
据我所知,只要这些模式仅用于检查用户选择的超文本锚点(“<a>”标签),此方法还可以防止插入“Web臭虫”。Web臭虫只是允许除主页的原始Web服务器之外的其他人跟踪信息(例如谁阅读了内容以及何时阅读了内容)的文本 - 有关Web臭虫的更多信息,请参阅第8.7节。如果您使用带有相同检查规则的<img>(图像)标记,则情况并非如此 - 图像标记会立即加载,从而允许某人添加“Web臭虫”。再次,这假定您不允许任何属性;许多属性可能非常危险,并会穿透您试图提供的安全性。
请注意,所有这些模式都要求整个URI与模式匹配。这些模式的一个不幸事实是,它们以禁止许多有用模式的方式限制了允许的模式(例如,它们阻止使用新的URI方案)。此外,它们都无法防止一些网站在收到查询时执行超出查询的操作的非常实际的问题 - 并且其中一些网站是组织内部的网站。因此,在没有任何Web站点接受GET查询作为操作之前,没有URI可以真正安全(请参阅第5.12节)。有关合法URL/URI的更多信息,请参阅IETF RFC 2396;域名语法在IETF RFC 1034中进一步讨论。
您甚至可以考虑支持更多HTML标签。明显的下一个选择是面向列表的标签,例如<ol>(有序列表),<ul>(无序列表)和<li>(列表项)。但是,在某个时候,您实际上是在允许完整的发布(在这种情况下,您需要信任提供商或执行比此处描述的更认真的检查)。更重要的是,您添加的每项新功能都会创造错误(和漏洞)的机会。
一个示例是允许使用相同URI模式的<img>(图像)标签。事实证明,这在很大程度上不太安全,因为这允许第三方将“Web臭虫”插入到文档中,从而识别出谁阅读了该文档以及何时阅读了该文档。有关Web臭虫的更多信息,请参阅第8.7节。
如果使用了来自不受信任用户的数据,则Web应用程序还应显式指定字符集(通常为ISO-8859-1),并且不允许其他字符。有关更多信息,请参阅第9.5节。
由于过滤此类输入很容易出错,因此也讨论了其他替代方案。一种选择是要求用户使用您设计的另一种语言,这种语言比HTML简单得多 - 并且您为该语言提供了非常有限的功能。另一种方法是将HTML解析为某种内部“安全”格式,然后将该安全格式转换回HTML。
过滤可以在输入,输出或两者期间完成。CERT建议在输出过程中,就在将其呈现为动态页面的一部分之前过滤数据。这是因为,如果正确完成,此方法可确保所有动态内容都被过滤。CERT认为,在输入端进行过滤的效果较差,因为动态内容可以通过HTTP以外的方法输入到网站数据库中,在这种情况下,Web服务器可能永远不会将数据视为输入过程的一部分。除非在输入动态数据的所有位置都实现了过滤,否则数据元素仍可能保持被污染。
但是,对于所有情况,我并不认同CERT的观点。问题在于,忘记过滤所有输出与忘记过滤输入一样容易,并且无论如何,允许“被污染的”输入进入您的系统都是一场等待发生的灾难。安全程序无论如何都必须过滤其输入,因此有时最好将所有这些检查都包含在输入过滤中(以便维护人员可以看到规则的真实情况)。最后,在某些安全程序中,可能有很多不同的程序位置可以输出值,但是只有极少数几种方法和位置可以将数据输入到其中;在这种情况下,在输入端进行过滤可能是更好的主意。
[1] | 从技术上讲,超文本链接可以是任何“统一资源标识符”(URI)。术语“统一资源定位符”(URL)是指URI的子集,该子集通过其主要访问机制(例如,其网络“位置”)的表示来标识资源,而不是通过名称或该资源的其他一些属性来标识资源。许多人使用术语“URL”作为“URI”的同义词,因为URL是最常见的URI类型。例如,URI中使用的编码实际上称为“URL编码”。 |