验证输入—接收用户数据的最佳实践方法
2003 年 7 月,计算机应急反应小组协调中心报告了 Microsoft Windows 的 DirectX MIDI 库中一组危险的漏洞。DirectXMIDI 库是用于播放 MIDI 格式音乐的底层 Windows 库。不幸的是,这个库没有能力去检查 MIDI 文件中的所有数据值;text、copyright 或者 MThd track 域中错误的值可以导致这个库的失效,而攻击者就可以利用这一漏洞让系统去执行他们想要执行的任何代码。这是特别危险的,因为 Internet Explorer 在察看一个包含 MIDI 文件链接的网页时,会自动加载那个文件并播放它。结果呢?一个攻击者只需要发布一个网页,当用户察看这个网页时,让用户的计算机删除所有的文件、把所有的机密文件通过电子邮件发送到其他地方、机器崩溃,或者去做任何攻击者想要做的事情。检查输入
在几乎所有安全的程序中,您的第一道防线就是检查您所接收到的每一条数据。如果您能不让恶意的数据进入您的程序,或者至少不在程序中处理它,您的程序在面对攻击时将更加健壮。这与防火墙保护计算机的原理很类似;它不能预防所有的攻击,但它可以让一个程序更加稳定。这个过程叫做检查、验证或者过滤您的输入。
一个明显的问题是,在何处执行检查?是在数据最初进入程序时,或者是在一个低层次的例程在实际使用这些数据时?通常,最好在这两处都对其进行检查;这样,即使一个攻击者成功地突破了一道防线,他们还会遇到另一条。最重要的规则是所有的数据必须在使用之前被检查。
误区:寻找不正确的输入
安全程序开发人员一个最大的误区是尝试去查找“非法的”数据值。这是不对的,因为攻击者非常聪明;他们常常会想到出其他的危险数据值。所以应该做的是确定哪些是合法的,检查数据是否符合定义,拒绝所有不符合定义的数据。为了安全,在开始时应该特别谨慎,只允许您知道合法的数据。毕竟,如果您限制的过于严格,用户很快就会报告说程序不允许合法的数据进入。另一方面,如果你限制的过于宽松,可能得直到程序被破坏您才会发现这一问题。
例如,我们假设您要基于用户的某个输入创建文件名。您可能知道不应该允许用户的输入中包括“/”,但是仅仅去检查这一个字符可能是不对的。比如,控制字符呢?空格会不会出问题?如果以破折号开头呢(在不好的代码中可能会出问题)?特别的短语会不会出问题?在绝大多数情况下,如果您创建了一个“非法”字符的列表,攻击者还是可以找到利用您的程序的方法。所以,应该检查并保证输入符合你认为是安全的特定模式,而拒绝不符合这个模式的所有输入。
确定出您所知道的危险值仍不失为一个好主意:您可以用它们(在头脑中)检查您的确认例程。这样,如果您知道使用“/”是危险的,就可以检查您的模式保证它不会让这个字符通过。
当然,所有这些都面临着一个问题:什么是合法的值?答案部分取决于您所期望的数据类型。所以接下来的几节我们将讨论程序要用到的几种通用数据类型??以及如何处理它们。
数字
我们从看起来最容易读的一类信息开始??数字。如果您期望输入的是一个数字,就确认数据是数字格式??比如,只是针对易做图数字,并且是至少一位易做图数字(您可以使用与正则表达式 ^[0-9]+$ 检查它)。在大多数情况下会有一个最小值和一个最大值;如果是这样,要确认数据在合法范围之内。
不要根据没有减号这一条件就认为不会有负数。在很多数据读取例程中,如果读到一个特别大的数,就会发生"溢出"而变成一个负数。实际上,一个非常聪明的针对 Sendmail 的攻击正是基于这一原理。Sendmail 会检查"调试标记"是不是比合法的值大,但是它并没有去检查这个值是不是负数。Sendamil 的开发者想当然地认为既然他们不允许使用减号,就不必再去检查输入是不是负数了。问题是数据读取例程会将大于 2^31 的数,比如4,294,967,269 ,转换成负数。攻击者可以利用这一点来覆盖至关重要的数据,并控制 Sendmail。
如果您读取的浮点数,还有另外需要关注的问题。许多设计用来读取浮点数的例程可能会允许“NaN”(非数字)这样的值。这样实际上会给接下来的处理例程带来问题,因为任何与这些数据比较的结果都会是假(而且,NaN 与 NaN 也不相等!)。您还需要知道标准 IEEE 浮点数的其他特殊定义,比如正无穷大和负无穷大,负零(还有正零)。所有您的程序没有考虑到的输入数据都有可能导致以后被利用。
字符串
同样,对于字符串您也要确定哪些是合法的,并拒绝所有其他的字符串。通常指定合法字符串最简单的方法是使用正则表达式:只需正确使用正则表达式编写描述哪些字符串合法的模式,抛弃那些不符合这个模式的数据。例如,^[A-Za-z0-9]+$ 指定字符串至少为一个字符长,而且只能包括大写字母、小写字母和易做图数字0到9(任意的顺序)。您可以使用正则表达式来更为详细地限制所允许的字符串(例如,您可以进一步指定第一个字符可以是哪些字母)。所有的语言都已实现正则表达式的库;Perl 是基于正则表达式的,对于 C,函数 regcomp(3) 和 regexec(3) 是POSIX.2 标准,并被广泛应用。
如果您使用正则表达式,一定要明确地指出您要匹配数据的的开始(通常用 ^ 来标识)和结束(通常用 $ 来标识)。如果您忘记了包括 ^ 或者 $,攻击者就可以在他们的攻击中嵌入合法的文本通过您的检查。如果您使用的 Perl,并且使用的它的多行选项(m),要注意:您必须使用 \A 来标识开始,用 \Z 来标识结束,因为多行操作改变了 ^ 和 $ 的含义。
最大的问题是如何明确地指出在字符串中哪些是合法的。通常,您应该尽可能地严格。有很多字符都会带来特定的问题;只要可能,您就不愿意允许在程序内部或者最终输出中有特定含义的那些字符。人们发现这确实很困难,因为在一些情况下有太多的字符可能会带来问题。
这里是经常会带来问题的字符的部分清单:
常规控制字符(字符值小于32): 还特别包括字符0,传统上称做 NUL;我把它称为 NIL 以区别于 C 语言中的 NULL 指针。在 C 语言中 NIL 标记了一个字符串的结束;即便您没有直接使用 C 语言,许多库会间接地去调用 C 语言的例程,如果给出了 NIL,就有可能出错。另一个问题可以被解释为命令结束的行结束符。不幸的是,有好几种行结束编码:基于 UNIX 的系统使用的是换行字符 (0x0a),但是基于 DOS 的系统(包括windows)使用的是 CP/M 的回车换行 (0x0d 0x0a),Apple MacOS 使用的是回车 (0x0d),许多 IBM 主机(比如 OS/390)使用的是下一行 (0x85),并且有一些程序甚至(错误地)使用反 CP/M 标记 (0x0a 0x0d)。
字符值大于127:这些是国际化的字符,但问题是它们可能会有许多可能的含义,所以您需要确保它们被正确地解释。通常这些都是 UTF-8 编码的字符,有其自身的复杂性;可以参考本文 后面关于 UTF-8 的讨论 。
元字符: 元字符是在您所依赖的程序或库中――比如命令 shell 或者 SQL――有特定含义的字符。
在您的程序中有特定含义的字符: 例如,用于定界的字符。许多程序将数据存放在文本文件中,使用逗号、制表符或者冒号隔开数据域;您需要拒绝含有这些值的数据或者对其进行编码。当前,一个常见的问题是小于号 (<),因为 XML 和 HTML 都用到了它。
这不是一个详尽的清单,并且您经常是需要接受它们中的一部分的。以后的文章将讨论当您不得不接收这些字符时如何处理它们。给出这个清单的目的是说服您尝试去接受尽可能少的数据,并且在接受另一个字符之前要慎重考虑。您接受的字符越少,您给攻击者制造的难度就越大。
更多的特殊数据类型
当然,还有更多的特殊数据类型。这里是对其中一部分的一些简要介绍。
文件名
如果数据是一个文件名(或者用于创建一个文件),应该对其进行严格限制。最好不要让用户来选择文件名,如果不得不那样做,那么把字符局限于形如 ^[A-Za-z0-9][A-Za-z0-9._\-]*$ 的较小模式。您应该考虑将“/”、控制字符(尤其生成新行的)和前导符“.”(UNIX/Linux 系统中的隐藏文件)等这些字符从合法模式中去掉。以“-”为前导也不好,因为写得不好的脚本会把它们解释为选项:如果有一个文件名为“-rf”,那么在 UNIX/Linux 中执行命令 rm *,将会变成执行 rm -rf *。将“../”从模式中去掉也是一个好主意,使攻击者无法“跳出”当前目录。如果可能,不要允许使用通配符(使用字符 *、?、[] 和 {} 来选择一组文件);攻击者可以通过创建稀奇古怪的通配模式来让系统不知如何处理而关闭。
Windows 还有另外一个问题:一些文件名(忽略扩展名和字母的大小写)总是被认为是物理设备。例如,如果一个程序在任何目录中试图去打开“COM1”或者甚至“com1.txt”,将被系统误解为是尝试和串口通信。由于我所关心的是类 UNIX 系统,我就不再深入地探讨如何解决这个问题了,而且这也没有什么意义,因为这只是一个例子,用来说明一种用于检查的合法字符不足的情况。
本地化
在当今全球经济的时候,许多程序都允许用户用于显示的语言和其他语言相关的特定信息(比如数字格式和字符编码)。程序通过用户提供一个“Locale”值来得到这一信息。例如,本地化参数值为“en_US.UTF-8”说明本地化参数使用的语言是 English,使用美国习惯,使用 UTF-8 编码。本地的类 UNIX 程序从环境变量中(通常是 LC_ALL,但可能更详细地分为 LC_COLLATE、LC_CTYPE、LC_MONETARY、LC_NUMERIC 和 LC_TIME;其他要检查的值是 NLSPATH、LANGUAGE、LANG 和 LINGUAS。)得到这一信息。网络应用可以通过接收语言请求的头信息或者别的方法来获得这个信息。