2.2 处理用户输入
回想一下第1章描述的基本安全问题:所有用户输入都不可信。大量针对Web应用程序的不同攻击都与提交错误输入有关,攻击者专门设计这类输入,以引发应用程序设计者无法预料的行为。因此,能够安全处理用户输入是对应用程序安全防御的一个关键要求。
应用程序的每一项功能以及几乎每一种常用的技术都可能出现输入方面的漏洞。通常来说,输入确认(input validation)是防御这些攻击的必要手段。然而,任何一种保护机制都不是万能的,防御恶意输入也并非如听起来那样简单。
2.2.1 输入的多样性
典型的Web应用程序以各种不同的形式处理用户提交的数据。一些类型的输入确认可能并不适用或能够确认所有这些形式的输入。通常由用户注册功能执行的输入确认如图2-4所示。
图2-4 应用程序正执行输入确认
在许多情况下,应用程序可能会对一些特殊的输入实行非常严格的确认检查。例如,提交给登录功能的用户名的最大长度为8个字符,且只能包含字母。
在其他情况下,应用程序必须接受更广泛的输入。例如,提交给个人信息页面的地址字段可合法包含字母、数字、空格、连字符、撇号与其他字符。但是仍然可以对这个字段实施有效的限制。例如,提交的数据不得超过某个适当的长度限制(如50个字符),并不得包含任何HTML标记(HTML markup)。
有些时候,应用程序可能需要接受用户提交的任意输入。例如,一名博客应用程序用户可以建立一个主题为“攻击Web应用程序”的博客。博客文章和评论可合法包含所讨论的明确攻击字符串。应用程序可能需要将这些输入保存在数据库中,写入磁盘,并以安全的方式向用户显示。不能仅仅因为输入看似恶意(但并未显著破坏应用程序对一些用户的价值),就拒绝接受该输入。
除了用户通过浏览器界面提交的各种输入外,一个典型的应用程序还会收到大量数据,它们在服务器上生成,并被传送给客户端,以便客户端能够在随后的请求中将其返回给服务器。这些数据包括cookie和隐藏表单字段,普通应用程序用户虽然无法浏览这些数据项,但攻击者能够查看并修改它们。在这些情况下,应用程序通常可对接收到的数据执行非常特殊的确认操作。例如,一个参数可能必须包含一个特殊的已知值(如说明用户首选语言的cookie),或者为某种特殊的格式(如一个顾客的身份证号码)。而且,如果应用程序发现服务器上生成的数据遭到修改,并且使用标准浏览器的普通用户根本不可能进行此类修改,那么极有可能是该用户正企图探查应用程序的漏洞。在这些情况下,应用程序应拒绝该用户提交的请求,并将事件记入日志文件中,以便随后进行调查(请参阅2.3节了解相关内容)。
2.2.2 输入处理方法
通常可采用各种方法来处理用户输入。不同的方法一般适用于不同的情形与不同类型的输入,有时最好结合采用几种方法。
1.“拒绝已知的不良输入”
这种方法一般使用一个黑名单,其中包含一组在攻击中使用的已知的字面量字符串或模式。确认机制阻止任何与黑名单匹配的数据,并接受其他数据。
一般来说,因为两方面的主要原因,这种方法是确认用户输入效率最低的方法。首先,攻击者可通过一系列输入对典型Web应用程序中存在的漏洞加以利用,这些输入可通过各种方式进行编码,或者表现为不同的形式。除非在最简单的情况下,否则,黑名单可能会忽略某些可用于攻击应用程序的输入模式。其次,攻击技术处在不断发展的过程之中。当前的黑名单无法防止利用现有漏洞的新型方法。
通过对被阻止的输入稍做调整,即可轻易避开许多基于黑名单的过滤。例如:
❑ 如果SELECT被阻止,则尝试SeLeCt;
❑ 如果or 1=1--被阻止,则尝试or 2=2--;
❑ 如果alert('xss')被阻止,则尝试prompt('xss')。
在其他情况下,通过在表达式之间使用非标准字符破坏应用程序执行的令牌,可以避开旨在阻止特定关键字的过滤。例如:
最后,各种基于黑名单的过滤,特别是那些由Web应用程序防火墙执行的过滤,都易受空字节攻击。由于在托管和非托管情况下处理字符串的方式各不相同,在被阻止的表达式之前的任何位置插入空字节可能导致某些过滤器停止处理输入,并因此无法确定表达式。例如:
我们将在第18章介绍各种攻击Web应用程序防火墙的其他技巧。
注解 对空字节的处理方式加以利用的攻击存在于Web应用程序安全的各个领域。在空字节被当做字符串分隔符的情况下,空字节可用于终止文件名或对某个后端组件的查询。在接受并忽略空字节的情况下(例如,在某些浏览器的HTML代码中),可以在被阻止的表达式中插入任意空字节,以避开基于黑名单的过滤。这类攻击将在后面几章详细介绍。
2.“接受已知的正常输入”
这种方法使用一个白名单,其中包含仅与良性输入匹配的一组字面量字符串、模式或一组标准。确认机制接受任何与白名单匹配的数据,并阻止其他数据。例如,在数据库中查询所需的产品代码时,应用程序可能会确认其仅包含字母数字字符,长度正好为6个字符。根据随后对产品代码进行的处理,开发者知道通过这种测试的输入不会造成任何问题。
在切实可行的情况下,这种方法是处理潜在恶意输入的最有效方法。因为在制定白名单时已经非常小心,所以攻击者无法使用专门设计的输入来干扰应用程序的行为。然而,在许多情况下,应用程序必须接受并不满足任何已知“正常”标准的数据,并对其进行处理。例如,在一些人的姓名中包含撇号和连字符的情况。这些数据可用于对数据库发动攻击。但也可能存在这样的要求,即应用程序应允许任何人以真实姓名注册。因此,虽然这种方法极其有效,但基于白名单的方法并非是解决处理用户输入问题的万能办法。
3.净化
这种方法认可有时需要接受无法保证其安全的数据。应用程序并不拒绝这种输入,相反,它以各种方式对其进行净化,防止它造成任何不利的影响。数据中可能存在的恶意字符被彻底删除掉,只留下已知安全的字符,或者在进一步处理前对它们进行适当编码或“转义”。
基于数据净化的方法一般非常有效。在许多情况下,可将其作为处理恶意输入问题的通用解决办法。例如,在将危险字符植入应用程序页面前对其进行HTML编码,是防御跨站点脚本攻击的常用方法(请参阅第12章了解相关内容)。然而,如果需要在一个输入项中容纳几种可能的恶意数据,可能就很难对其进行有效的净化。这时,最好采用边界确认方法处理用户输入,如后文所述。
4.安全数据处理
以不安全的方式处理用户提交的数据,是许多Web应用程序漏洞形成的根本原因。通常,不需要确认输入本身,只需确保处理过程绝对安全,即可避免这些漏洞。有些时候,可使用安全的编程方法避免常见问题。例如,在数据库访问过程中正确使用参数化查询,就可以避免SQL注入攻击(请参阅第9章了解相关内容)。在其他情况下,完全可以避免应用程序功能设计不安全的做法,如向操作系统命令解释程序提交用户输入。
这种方法并不适用于Web应用程序需要执行的每项任务,但如果适用,它是一种有效处理潜在恶意输入的通用方法。
5.语法检查
迄今为止,本书描述的防御措施全都用于防止应用程序接受各种错误的输入,攻击者专门设计这些输入的内容以干扰应用程序的处理过程。然而,在一些漏洞中,攻击者提交的输入与普通的非恶意用户提交的输入完全相同。之所以称其为恶意输入,是因为攻击者提交的动机不同。例如,攻击者可能会修改通过隐藏表单字段提交的账号,企图访问其他用户的银行账户。这时,再多的语法确认也无法区别用户与攻击者的数据。为防止未授权访问,应用程序必须确认所提交的账号属于之前提交该账号的用户。
2.2.3 边界确认
在信任边界确认数据的做法并不少见。用户提交的数据不可信是造成Web应用程序核心安全问题的主要原因。虽然在客户端执行的输入确认检查可以提高性能,改善用户体验,但它们并不能为实际到达服务器的数据提供任何保证。服务器端应用程序第一次收到用户数据的地方是一个重要的信任边界,应用程序需要在此采取措施防御恶意输入。
鉴于核心问题的本质,可以基于因特网(“不良”且不可信)与服务器端应用程序(“正常”且可信)之间的边界来考虑输入确认问题。从这个角度看,输入确认的任务就是净化到达的潜在恶意数据,然后将“洁净的”数据提交给可信的应用程序。此后,数据即属于可信数据,不需要任何进一步的检查或担心可能的攻击,即可进行处理。
很明显,当我们开始分析一些实际的漏洞时,执行这种简单的输入确认是不够的,原因如下。
❑ 基于应用程序所执行功能的广泛性以及其所采用技术的多样性,一个典型的应用程序需要防御大量各种各样的基于输入的攻击,且每种攻击可能采用一组截然不同的专门设计的数据。因此,很难在外部边界建立一个单独的机制,防御所有这些攻击。
❑ 许多应用程序功能都涉及组合一系列不同类型的处理过程。用户提交的一项输入可能会在不同的组件中引发许多操作,其中前一个操作的输出结果被用于后一个操作的输入。数据发生转换后,可能会变得与原始的输入完全不同。而经验丰富的攻击者能够操纵应用程序,在关键处理阶段生成恶意输入,攻击接收这些数据的组件。为此,很难在外部边界执行确认机制,预测每一个用户输入的全部可能处理结果。
❑ 防御不同类型的基于输入的攻击可能需要对相互矛盾的用户输入执行各种确认检查。例如,防止跨站点脚本攻击可能需要将> 字符HTML编码为>,而防止命令注入攻击则需要阻止包含& 与;字符的输入。有时候,想要在应用程序的外部边界同时阻止所有类型的攻击几乎是不可能的事情。
边界确认(boundary validation)是一种更加有效的模型。此时,服务器端应用程序的每一个单独的组件或功能单元将其输入当做来自潜在恶意来源的输入对待。除客户端与服务器之间的外部边界外,应用程序在上述每一个信任边界上执行数据确认。这种模型为前面提出的问题提供了一个解决方案。每个组件都可以防御它收到的特殊类型的专门设计的输入。当数据通过不同的组件时,即可对前面转换过程中生成的任意数据值执行确认检查。而且,由于在不同的处理阶段执行不同的确认检查,它们之间不可能发生冲突。
图2-5说明了一种典型情况,此时边界确认是防御恶意输入的最有效方法。在用户登录过程中,需要对用户提交的输入进行几个步骤的处理,并在每个步骤执行适当的确认检查。
图2-5 一种在多阶段处理步骤中使用边界确认的应用程序功能
(1) 应用程序收到用户的登录信息。表单处理程序确认每个输入仅包含合法字符,符合特殊的长度限制,并且不包含任何已知的攻击签名。
(2) 应用程序执行一个SQL查询检验用户证书。为防止SQL注入攻击,在执行查询前,应用程序应对用户输入中包含的可用于攻击数据库的所有字符进行转义。
(3) 如果用户成功登录,应用程序再将用户资料中的某些数据传送给SOAP服务,进一步获得用户账户的有关信息。为防止SOAP注入攻击,需要对用户资料中的任何XML元字符进行适当编码。
(4) 应用程序在用户的浏览器中显示用户的账户信息。为防止跨站点脚本攻击,应用程序对植入返回页面的任何用户提交的数据执行HTML编码。
我们将在后续章节详细介绍上文描述的特殊漏洞和防御机制。如果这一功能发生变化,需要向其他应用程序组件提交数据,那么可能需要在相关信任边界执行类似的防御。例如,如果登录失败致使应用程序向用户发送警告电子邮件,那么可能需要检查合并到电子邮件中的所有用户数据,防止SMTP注入攻击。
2.2.4 多步确认与规范化
在确认检查过程中,当需要在几个步骤中处理用户提交的输入时,就会出现一个输入处理机制经常遇到的问题。如果不谨慎处理这个过程,那么攻击者就能够建立专门设计的输入,使恶意数据成功避开确认机制。当应用程序试图通过删除或编码某些字符或表达式净化用户输入时,就会出现这种问题。例如,为防御某些跨站点脚本攻击,应用程序可能会从任何用户提交的数据中删除表达式:
但攻击者可通过应用以下输入避开过滤器:
由于过滤无法递归运行,删除被阻止的表达式后,表达式周围的数据又合并在一起,重新建立恶意表达式。
同样,如果对用户输入执行几个确认步骤,攻击者就可以利用这些步骤的顺序来避开过滤。例如,如果应用程序首先递归删除../,然后递归删除..\,就可以使用以下输入避开确认检查:
数据规范化(data canonicalization)会造成另一个问题。当用户浏览器送出输入时,它可对这些输入进行各种形式的编码。之所以使用这些编码方案,是为了能够通过HTTP安全传送不常见的字符与二进制数据(请参阅第3章了解更多详情)。规范化是指将数据转换或解码成一个常见字符集的过程。如果在实施输入过滤之后才执行规范化,那么攻击者就可以通过使用编码避开确认机制。
例如,应用程序可能会从用户输入中删除省略号,以防止某些SQL注入攻击。但是,如果应用程序随后对净化后的数据进行规范化,那么攻击者就可以使用URL编码的输入避开确认:
收到该输入后,应用程序服务器会执行正常的URL解码,因此该输入变为:
其中并不包含省略号,因此,应用程序的过滤器允许该输入。但是,如果应用程序执行进一步的URL解码,该输入将变为省略号,从而避开过滤。
如果应用程序删除而不是阻止省略号,然后执行进一步的规范化,则可以使用以下输入避开过滤:
值得注意的是,在这些情况下,应用程序服务器端不一定会执行多步确认和规范化。例如,在下面的输入中,几个字符已被HTML编码:
如果服务器端应用程序使用输入过滤来阻止某些JavaScript表达式和字符,该已编码的输入就可以成功避开过滤。但是,如果该输入随后被复制到应用程序的响应中,某些浏览器将对src参数值执行HTML解码,嵌入的JavaScript将得以执行。
除了供Web应用程序使用的标准编码方案外,其他情况下,如果应用程序采用的组件将数据从一个字符集转换为另一个字符集,这也会导致规范化问题。例如,某些技术会基于印刷字形的相似性,对字符执行“最佳”映射。这时,字符<<和>>分别被转换为<和>, Ÿ和Â则被转换为Y和A。攻击者经常利用这种方法传送受阻止的字符或关键字,从而避开应用程序的输入过滤。
本书将详细介绍这类攻击,它们可有效挫败应用程序针对常见的基于输入的漏洞而采取的许多防御机制。
有时候,可能很难避免多步确认与规范化造成的问题,也不存在解决这类问题的唯一方案。一种解决办法是递归执行净化操作,直到无法进一步修改输入。然而,如果需要在净化过程中对一个存在疑问的字符进行转义,那么这种情况可能会造成无限循环。通常,这个问题只有根据具体情况、基于所执行的确认类型加以解决。如果可能,最好避免净化某些不良输入的做法,完全拒绝这种类型的输入。