用户可控制的数据未经适当确认与净化就被复制到应用程序响应中,这是造成反射型与保 存型XSS漏洞的根本原因。由于数据被插入到一个HTML页面的源代码中,恶意数据就会干扰这个页面,不仅修改它的内容,还会破坏它的结构(影响引用字符串、起始与结束标签、注入脚本等)。
为消除反射型与保存型XSS漏洞,首先必须确定应用程序中用户可控制的数据被复制到响应中的每一种情形。这包括从当前请求中复制的数据以及用户之前输入的保存在应用程序中的数据,还有通过带外通道输入的数据。为确保确定每一种情形,除仔细审查应用程序的全部源代码外,没有其他更好的办法。
确定所有可能存在XSS风险、需要适当进行防御的操作后,需要采取一种三重防御方法阻止漏洞的发生。这种方法由以下3个因素组成:
确认输入;
确认输出;
消除危险的插入点。
如果应用程序需要允许用户以HTML格式创建内容(如允许在注释中使用HTML的博客应用程序),这时应谨慎使用这种方法。我们将在介绍常规防御技巧后讨论与这种情况有关的一些特定注意事项。
1.确认输入
如果应用程序在某个位置收到的用户提交的数据将来有可能被复制到它的响应中,应用程序应根据这种情形对这些数据执行尽可能严格的确认。可能需要确认的数据的特性包括以下几点。
数据不是太长。
数据仅包含某组合法字符。
数据与一个特殊的正规表达式相匹配。
根据应用程序希望在每个字段中收到的数据类型,应尽可能限制性地对姓名、电子邮件地址、账号等应用不同的确认规则。
2.确认输出
如果应用程序将某位用户或第三方提交的数据复制到它的响应中,那么应用程序应对这些数据进行HTML编码,以净化可能的恶意字符。HTML编码指用对应的HTML实体替代字面量字符。这样做可确保浏览器安全处理可能为恶意的字符,把它们当做HTML文档的内容而非结构处理。一些经常造成问题的字符的HTML编码如下:
"——"
'——'
&——&
<—&md&;<
>——>
除这些常用的编码外,实际上,任何字符都可以用它的数字ASCII字符代码进行HTML编码,举例如下:
%——%
应该注意,在将用户输入插入到标签属性值中时,浏览器会首先对该值进行HTML解码,然后执行其他处理。在这种情况下,仅仅对任何在正常情况下存在问题的字符进行HTML编码的防御机制可能会失效。确实,如前所述,对于某些过滤,攻击者可以避免在有效载荷中使用HTML编码的字符。例如:
我们在下一节将会讲到,最好是避免在这些位置插入用户可控制的数据。如果在某些情况必须这样做,在执行操作时应特别小心,以防止任何可以避免过滤的情况。例如,如果将用户输入插入到事件处理器中的引用JavaScript字符串中,应使用反斜线正确转义用户输入中的任何引号或反斜线,并且HTML编码应包括&和;字符,以防止攻击者自己执行HTML编码。
在将用户可控制的字符串复制到服务器的响应中之前,ASP.NET应用程序可以使用Server.HTMLEncode API净化其中的常见恶意字符。这个API把字符"&<和>转换成它们对应的HTML实体,并且使用数字形式的编码转换任何大于0x7f的ASCII字符。
在Java平台中没有与之等效的API;但是可以使用数据形式的编码构造自己的等效方法。例如:
当处理用户提交的数据时,开发者常常会犯一个错误,即仅对在特殊情况下对攻击者有用的字符进行HTML编码。例如,如果数据被插入到一个双引号引用的字符串中,应用程序可能只编码"字符;如果数据被插入到一个没有引号的标签中,应用程序只会编码>字符。这种方法明显增加了攻击者避开确认的风险。攻击者常常利用浏览器接受无效HTML与JavaScript的弱点,改变确认情境或以意外的方式注入代码。而且,攻击者可以将一个攻击字符串分布到几个可控制的字段中,利用应用程序对每个字段采用的不同过滤避开其他过滤。一种更加可靠的方法是,无论数据插入到什么地方,始终对攻击者可能使用的每一个字符进行HTML编码。为尽可能地确保安全,开发者可能会选择HTML编码每一个非字母数字字符,包括空白符。这种方法通常会显著增加应用程序的工作压力,同时给任何尝试避开过滤的攻击设置巨大障碍。
应用程序之所以结合使用输入确认与输出净化,原因在于这种方法能够提供两层防御:如果其中一层被攻破,另一层还能提供一些保护。如上文所述,许多执行输入与输出确认的过滤都容易被攻破。结合这两种技巧,应用程序就能够获得额外的保护,即使攻击者发现其中一种过滤存 在缺陷,另一种过滤仍然能够阻止他实施攻击。在这两种防御中,输出确认更为重要,必不可少。实施严格的输入确认应被视为一种次要故障恢复(secondary failover)。
当然,当设计输入与输出确认机制时,我们应特别小心,尽量避免任何可能导致攻击者避开确认的漏洞。尤其要注意的是,应在实施相关规范化后再对数据进行过滤与编码,而且之后不得对数据实施进一步的规范化。应用程序还必须保证其中存在的空字节不会对它的确认造成任何干扰。
3.消除危险的插入点
应用程序页面中有一些位置,在这里插入用户提交的输入就会造成极大的风险;因此,开发者应力求寻找其他方法执行必要的功能。
应尽量避免直接在现有的JavaScript中插入用户可控制的数据。这适用于<script>标签中的代码,也适用于事件处理器的代码。如果应用程序尝试以安全的方式在其中插入数据,可能就会使攻击者有机会避开它们实施的防御性过滤。一旦攻击者能够控制提交数据的插入点,他不用付出多大努力就可以注入任意脚本命令,从而实施恶意操作。
如果标签属性接受URL作为它的值,通常应用程序应该避免嵌入用户输入,因为各种技巧也能引入脚本代码,包括伪协议脚本处理的使用。
如果攻击者通过插入一个相关指令,或者因为应用程序使用一个请求参数指定首选的字符集,因而能够控制应用程序响应的编码类型,那么这些情况也应该加以避免。在这种情况下,在其他方面经过精心设计的输入与输出过滤可能就会失效,因为攻击者的输入进行了不常见的编码,以致上述过滤并不将其视为恶意输入。只要有可能,应用程序应在它的响应消息头中明确指定一种编码类型,禁止对它进行任何形式的修改,并确保应用程序的XSS过滤与其兼容。例如:
4.允许有限的HTML
一些应用程序需要允许用户以HTML格式提交即将插入到应用程序响应中的数据。例如,博客应用程序可能需要允许用户使用HTML撰写博客、对博客使用格式、嵌入链接或图像等。在这种情况下,不作区分地应用上述措施将会导致错误。用户的HTML标记将在响应中被HTML编码,因此作为真实的标记显示在屏幕上,而不是以所需的格式化内容显示。
为安全地支持这种功能,应用程序需要保持稳健,仅允许有限的HTML子集,避免提供任何引入脚本代码的方法。这包括采用一种白名单方法,仅允许特定的标签和属性。成功做到这一点并不简单,如前所述,攻击者可以通过各种方法使用看似无害的标签来执行代码。
例如,如果应用程序允许使用<b>和<i>标签,但并不限制与这些标签一起使用的属性,则攻击者可以实施以下攻击:
此外,如果应用程序允许使用看似安全的<a>标签和href属性的组合,则攻击者可以实施以下攻击:
有各种框架(如OWASP AntiSamy项目)可用于确认用户提交的HTML标记,以确保其中不包含任何执行JavaScript的方法。建议需要允许用户创建有限HTML的开发者直接使用某个成熟的框架,或仔细分析其中一种框架,以了解面临的各种相关挑战。
或者,也可以采用某种定制的中间标记语言,允许用户使用有限的中间语言语法,然后由应用程序对其进行处理,以生成相应的HTML标记。
很明显,迄今为止,我们描述的防御机制并不能防止基于DOM的XSS漏洞,因为造成这种漏洞并不需要将用户可控制的数据复制到服务器响应中。
应用程序应尽量避免使用客户端脚本处理DOM数据并把它插入到页面中。由于被处理的数据不在服务器的直接控制范围内,有时甚至不在它的可见范围内,因此这种行为存在着固有的风险。
如果无法避免地要以这种方式使用客户端脚本,我们可以通过两种防御方法防止基于DOM的XSS漏洞,它们分别与前面描述的防止反射型XSS漏洞时使用的输入与输出确认相对应。
1.确认输入
许多时候,应用程序可以对它处理的数据执行严格的确认。确实,在这方面,客户端确认比服务器端确认更加有效。在前面描述的易受攻击的示例中,我们可以通过确认将要插入到文档中的数据仅包含字母数字字符与空白符,从而阻止攻击发生。例如:
除这种客户端控制外,还可以在服务器端对URL数据进行严格的确认,实施深层防御,以检测利用基于DOM的XSS漏洞的恶意请求。在刚刚说明的同一个示例中,应用程序甚至只需实施服务器端数据确认,通过确认以下数据来阻止攻击:
查询字符串中只有一个参数;
参数名为message(大小写检查);
参数值仅包含字母数字内容。
实施了这些控制后,客户端脚本仍有必要正确解析出message参数的值,确保其中并不包含任何URL片断字符。
2.确认输出
与防止反射型XSS漏洞时一样,在将用户可控制的DOM数据插入到文档之前,应用程序也可以对它们进行HTML编码。这样就可以将各种危险的字符与表达式以安全的方式显示在页面中。例如,使用下面的函数即可在客户端JavaScript中执行HTML编码: