智慧必能保你脱离恶道,救你脱离说乖谬话的人... | |
箴言 2:12 (NIV) |
一些输入来自不可信任的用户,因此这些输入在使用前必须经过验证(过滤)。您应该确定什么是合法的,并拒绝任何不符合该定义的输入。不要反过来做(识别什么是非法的并编写代码来拒绝这些情况),因为您很可能忘记处理非法输入的重要情况。
不过,识别“非法”值有一个很好的理由,那就是作为一组测试(通常只是在您脑海中执行)来确保您的验证代码是彻底的。当我设置输入过滤器时,我会从思想上攻击过滤器,看看是否有非法值可以通过。根据输入的不同,以下是一些常见的“非法”值示例,您的输入过滤器可能需要阻止:空字符串、“.”、“..”、“../”、任何以“/”或“.”开头的内容、任何内部包含“/”或“&”的内容、任何控制字符(尤其是 NIL 和换行符)和/或任何设置了“高位”的字符(尤其是十进制值 254 和 255,字符 133 是 OS/390 使用的 Unicode 行分隔符)。同样,您的代码不应检查“坏”值;您应该在思想上进行此检查,以确保您的模式无情地将输入值限制为合法值。如果您的模式不够窄,您需要仔细重新检查模式,看看是否还有其他问题。
限制最大字符长度(以及适当的最小长度),并确保在超出此类长度时不会失去控制(有关缓冲区溢出的更多信息,请参阅第 6 章)。
以下是一些常见的数据类型,以及您在使用来自不受信任的用户的数据之前应验证的事项
对于字符串,请识别合法字符或合法模式(例如,作为正则表达式),并拒绝任何与该形式不匹配的内容。当字符串包含控制字符(尤其是换行符或 NIL)或元字符(尤其是 shell 元字符)时,会出现特殊问题;通常最好在收到输入时立即“转义”此类元字符,以便此类字符不会意外发送。CERT 更进一步,建议转义所有不在不需要转义的字符列表中的字符 [CERT 1998, CMU 1998]。有关元字符的更多信息,请参阅第 8.3 节。请注意,不同计算机上的行尾编码有所不同:基于 Unix 的系统使用字符 0x0a(换行符),基于 CP/M 和 DOS 的系统(包括 Windows)使用 0x0d 0x0a(回车换行,一些程序错误地颠倒了顺序),Apple MacOS 使用 0x0d(回车),IBM OS/390 使用 0x85 (0x85)(下一行,有时称为换行符)。
将所有数字限制为最小(通常为零)和最大允许值。
完整的电子邮件地址检查器实际上非常复杂,因为如果您需要支持所有旧格式,则旧格式会大大复杂化验证;如果需要此类检查,请参阅 mailaddr(7) 和 IETF RFC 822 [RFC 822] 以获取更多信息。Friedl [1997] 开发了一个正则表达式来检查电子邮件地址是否有效(根据规范);他的“短”正则表达式有 4,724 个字符,他的“优化”表达式(在附录 B 中)有 6,598 个字符长。即使是该正则表达式也不是完美的;它无法识别本地电子邮件地址,也无法处理注释中的嵌套括号(规范允许)。通常,您可以简化并仅允许“常用” Internet 地址格式。
应检查文件名;有关文件名的更多信息,请参阅第 5.4 节。
应检查 URI(包括 URL)的有效性。如果您直接对 URI 执行操作(即,您正在实现 Web 服务器或类似 Web 服务器的程序,并且 URL 是对您的数据的请求),请确保 URI 有效,并特别注意尝试“逃逸”文档根目录(服务器正在响应的文件系统区域)的 URI。逃逸文档根目录最常见的方法是通过“..”或符号链接,因此大多数服务器都会检查任何“..”目录本身,并忽略符号链接,除非另有明确指示。还要记住首先解码任何编码(通过 URL 编码或 UTF-8 编码),否则编码的“..”可能会溜过。URI 甚至不应该包含 UTF-8 编码,因此最安全的方法是拒绝任何包含设置了高位的字符的 URI。
如果您正在实现一个使用 URI/URL 作为数据的系统,那么您根本没有完成任务;您需要确保恶意用户无法插入会伤害其他用户的 URI。有关此方面的更多信息,请参阅第 5.11.4 节。
当接受 cookie 值时,请确保检查您正在使用的任何 cookie 的域值是否是预期的值。否则,一个(可能被破解的)相关站点可能能够插入欺骗性 cookie。以下是 IETF RFC 2965 中的一个示例,说明未能执行此检查可能会导致问题
用户代理向 victim.cracker.edu 发出请求,收到 cookie session_id="1234" 并设置默认域 victim.cracker.edu。
用户代理向 spoof.cracker.edu 发出请求,收到 cookie session-id="1111",域=".cracker.edu"。
用户代理再次向 victim.cracker.edu 发出请求,并传递
Cookie: $Version="1"; session_id="1234", $Version="1"; session_id="1111"; $Domain=".cracker.edu" |
除非您考虑到它们,否则合法字符模式不得包含对程序内部或最终输出具有特殊含义的字符或字符序列
字符序列可能对程序的内部存储格式具有特殊含义。例如,如果您以分隔字符串的形式(内部或外部)存储数据,请确保分隔符不是允许的数据值。许多程序以逗号 (,) 或冒号 (:) 分隔的文本文件存储数据;除非程序考虑到它(即,通过阻止它或以某种方式编码它),否则在输入中插入分隔符可能会成为问题。其他经常引起这些问题的字符包括单引号和双引号(用于包围字符串)以及小于号“<”(在 SGML、XML 和 HTML 中用于指示标签的开始;如果您以这些格式存储数据,这很重要)。大多数数据格式都有转义序列来处理这些情况;使用它,或在输入时过滤此类数据。
如果字符序列被发送回给用户,则它可能具有特殊含义。一个常见的例子是在数据输入中允许 HTML 标签,这些标签稍后将发布给其他读者(例如,在留言簿或“读者评论”区域中)。然而,问题远不止于此。有关该主题的一般讨论,请参阅第 7.15 节,有关过滤 HTML 的具体讨论,请参阅第 5.11 节。
这些测试通常应集中在一个地方,以便以后可以轻松检查验证测试的正确性。
确保您的有效性测试实际上是正确的;当检查将由另一个程序使用(例如文件名、电子邮件地址或 URL)的输入时,这尤其是一个问题。通常,这些测试存在细微的错误,从而产生所谓的“代理问题”(其中检查程序做出的假设与实际使用数据的程序不同)。如果有相关的标准,请查看它,但也搜索以查看程序是否有您需要了解的扩展。
在解析用户输入时,最好暂时放弃所有权限,甚至创建单独的进程(解析器永久放弃权限,另一个进程根据解析器请求执行安全检查)。如果解析任务很复杂(例如,如果您使用类似 lex 或 yacc 的工具),或者如果编程语言不能防止缓冲区溢出(例如,C 和 C++),则尤其如此。有关最小化权限的更多信息,请参阅第 7.4 节。
当使用数据进行安全决策时(例如,“让此用户进入”),请务必使用可信通道。例如,在公共 Internet 上,不要仅使用机器 IP 地址或端口号作为验证用户的唯一方法,因为在大多数环境中,此信息可以由(潜在的恶意)用户设置。有关更多信息,请参阅第 7.11 节。
以下小节讨论了程序的各种输入类型;请注意,输入包括进程状态,例如环境变量、umask 值等等。并非所有输入都在不受信任的用户的控制之下,因此您只需要担心那些受控制的输入。