5.11. 过滤可能被重新呈现的HTML/URI

一个必须防止跨站恶意内容的特殊情况是,Web应用程序被设计为接受来自一个用户的HTML或XHTML,然后将其发送给其他用户(有关跨站恶意内容的更多信息,请参阅第7.15节)。以下小节讨论过滤这种特定类型的输入,因为处理它是如此常见的需求。

5.11.1. 移除或禁用某些HTML数据

最安全的方法是移除所有可能的(X)HTML标签,这样它们就不会影响任何东西,而且这相对容易做到。如上所述,您应该已经识别了合法字符的列表,并拒绝或移除列表中不存在的字符。在这个过滤器中,只需不要将以下字符包含在合法字符列表中:``<'',``>''和``&''(如果它们在属性中使用,则包括双引号字符``"''')。如果浏览器仅根据HTML规范运行,则无需移除``>'',但在实践中必须移除。这是因为某些浏览器假定页面作者真的想输入一个开头的“<”,并“乐于助人”地插入一个 - 攻击者可以利用这种行为并使用“>”来创建不希望出现的“<”。

通常,传输HTML的字符集是ISO-8859-1(即使发送国际文本也是如此),因此过滤器还应省略大多数控制字符(换行符和制表符通常可以接受)以及高位已设置的字符。

这种方法的一个问题是,它可能会真正让用户感到惊讶,特别是那些输入国际文本的用户,如果所有国际文本都被悄悄移除。如果无效字符被悄悄移除而没有警告,则数据将不可挽回地丢失,并且以后无法重建。一种替代方法是禁止此类字符,并将错误消息发送回尝试使用它们的用户。这至少会警告用户,但不会给他们提供他们正在寻找的功能。其他替代方法是编码此数据或验证此数据,这将在接下来讨论。

5.11.2. 编码HTML数据

一种几乎同样安全的替代方法是转换关键字符,使其在HTML中不会具有通常的含义。这可以通过将所有“<”转换为“&lt;”,“>”转换为“&gt;”,以及“&”转换为“&amp;”来完成。任意国际字符可以使用格式“&#value;”以Latin-1编码 - 不要忘记结尾的分号。对国际字符进行编码意味着您必须知道输入编码是什么,当然。

这里可能存在的一个危险是,如果这些编码被意外地解释两次,它们将成为漏洞。但是,这种方法至少允许后来的用户看到输入的“意图”。

5.11.3. 验证HTML数据

某些应用程序为了正常工作,必须接受来自第三方的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之类的东西,可能会给您的用户带来麻烦。您会注意到,在上面的列表中,我根本没有包含任何属性,这当然是最安全的做法。如果使用了不安全的标签,您应该发出警告消息,但如果这不切实际,则编码关键字符(例如,“<”变为“&lt;”)可以防止数据丢失,同时保持用户安全。

在扩展此集合时要小心,通常要限制您接受的内容。如果您的模式过于宽松,则浏览器可能会以与您期望不同的方式解释序列,从而导致潜在的漏洞。例如,FozZy在Bugtraq(2002年4月1日)上发布了一些序列,这些序列允许在各种基于Web的邮件系统中进行利用,这可能会让您了解您需要防御的各种问题。这是一些漏洞利用文本,曾经可以颠覆Microsoft Hotmail中的用户帐户
   <SCRIPT>
   </COMMENT>
   <!-- --> -->
这是一些类似的Yahoo! Mail漏洞利用文本
  <_a<script>
  <<script>        (Note: this was found by BugSan)
这是一些Vizzavi漏洞利用文本
  <b onmousover="...">go here</b>
  <img [line_break] src="javascript:alert(document.location)">
Andrew Clover在Bugtraq(2002年5月11日)上发布了各种文本的列表,这些文本调用Javascript但设法绕过许多过滤器。以下是他的示例(他说他是从其他地方剪切并粘贴的);有些仅适用于特定的浏览器(IE表示Internet Explorer,N4表示Netscape版本4)。
  <a href="javas&#99;ript&#35;[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:<s&#99;ript>[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>&lt;script>[code]&lt;/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);我没有仔细检查它,但您可能想看一下。

5.11.4. 验证超文本链接(URI/URL)

细心的读者会注意到,我没有将超文本链接标签<a>作为HTML中的安全标签包含在内。显然,您可以将<a href="safe URI">(超文本链接)添加到安全列表中(除非您已检查其内容,否则不允许任何其他属性)。如果您的应用程序需要它,请这样做。但是,允许第三方创建链接的安全性要低得多,因为定义“安全URI”[1]被证明非常困难。许多浏览器接受各种可能对用户有害的URI。本节讨论如何验证来自第三方的URI以重新呈现给其他人,包括合并到HTML中的URI。

首先,让我们简要地看一下URI语法(如各种规范所定义)。URI可以是“绝对”的或“相对”的。绝对URI的语法如下所示
scheme://authority[path][?query][#fragment]
URI以方案名称(例如“http”)开头,字符“://”,授权(例如“www.dwheeler.com”),路径(看起来像目录或文件名),问号后跟查询,以及哈希符号(“#”)后跟片段标识符。方括号包围可选部分 - 例如,许多URI实际上不包括查询或片段。某些方案可能不允许某些数据(例如,路径,查询或片段),并且许多方案具有它们特有的其他要求。许多方案允许“授权”字段标识可选的用户名,密码和端口,使用以下语法表示“授权”部分
 [username[:password]@]host[:portnumber]
“主机”可以是名称(“www.dwheeler.com”)或IPv4数字地址(127.0.0.1)。“相对”URI引用相对于“当前”对象的一个对象,其语法看起来很像文件名
path[?query][#fragment]
大多数URI中允许的字符数量有限,因此为了解决此问题,其他8位字符可以“URL编码”为%hh(其中hh是8位字符的十六进制值)。有关有效URI的更多详细信息,请参阅IETF RFC 2396及其相关规范。

现在我们已经了解了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\-\_\.\!\~\*\'\(\)\%\?]+))*/?
此模式实际上不是很智能,因为它允许非法转义,多个查询,ftp中的查询等等。它确实具有相对简单的优点。

创建一个“有点安全”模式,真正将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中进一步讨论。

5.11.5. 其他HTML标签

您甚至可以考虑支持更多HTML标签。明显的下一个选择是面向列表的标签,例如<ol>(有序列表),<ul>(无序列表)和<li>(列表项)。但是,在某个时候,您实际上是在允许完整的发布(在这种情况下,您需要信任提供商或执行比此处描述的更认真的检查)。更重要的是,您添加的每项新功能都会创造错误(和漏洞)的机会。

一个示例是允许使用相同URI模式的<img>(图像)标签。事实证明,这在很大程度上不太安全,因为这允许第三方将“Web臭虫”插入到文档中,从而识别出谁阅读了该文档以及何时阅读了该文档。有关Web臭虫的更多信息,请参阅第8.7节

5.11.6. 相关问题

如果使用了来自不受信任用户的数据,则Web应用程序还应显式指定字符集(通常为ISO-8859-1),并且不允许其他字符。有关更多信息,请参阅第9.5节

由于过滤此类输入很容易出错,因此也讨论了其他替代方案。一种选择是要求用户使用您设计的另一种语言,这种语言比HTML简单得多 - 并且您为该语言提供了非常有限的功能。另一种方法是将HTML解析为某种内部“安全”格式,然后将该安全格式转换回HTML。

过滤可以在输入,输出或两者期间完成。CERT建议在输出过程中,就在将其呈现为动态页面的一部分之前过滤数据。这是因为,如果正确完成,此方法可确保所有动态内容都被过滤。CERT认为,在输入端进行过滤的效果较差,因为动态内容可以通过HTTP以外的方法输入到网站数据库中,在这种情况下,Web服务器可能永远不会将数据视为输入过程的一部分。除非在输入动态数据的所有位置都实现了过滤,否则数据元素仍可能保持被污染。

但是,对于所有情况,我并不认同CERT的观点。问题在于,忘记过滤所有输出与忘记过滤输入一样容易,并且无论如何,允许“被污染的”输入进入您的系统都是一场等待发生的灾难。安全程序无论如何都必须过滤其输入,因此有时最好将所有这些检查都包含在输入过滤中(以便维护人员可以看到规则的真实情况)。最后,在某些安全程序中,可能有很多不同的程序位置可以输出值,但是只有极少数几种方法和位置可以将数据输入到其中;在这种情况下,在输入端进行过滤可能是更好的主意。

注释

[1]

从技术上讲,超文本链接可以是任何“统一资源标识符”(URI)。术语“统一资源定位符”(URL)是指URI的子集,该子集通过其主要访问机制(例如,其网络“位置”)的表示来标识资源,而不是通过名称或该资源的其他一些属性来标识资源。许多人使用术语“URL”作为“URI”的同义词,因为URL是最常见的URI类型。例如,URI中使用的编码实际上称为“URL编码”。