SecureReality 发布了一篇非常有趣的论文,题为“深红研究 - 利用 PHP 中的常见漏洞”[Clowes 2001],其中讨论了使用 PHP 编写安全程序时的一些问题,尤其是在 PHP 4.1.0 之前的版本中。Clowes 总结说,“即使你尝试,也很难编写一个安全的 PHP 应用程序(在 PHP 的默认配置中)”。
诚然,任何语言都存在安全问题,但 PHP 早期版本中一个特别突出的问题,可以说使早期 PHP 版本比大多数语言都更不安全:它将数据加载到其命名空间的方式。默认情况下,在 PHP(4.1.0 及更低版本)中,所有环境变量和通过网络发送到 PHP 的值都会自动加载到与普通变量加载到的相同命名空间(全局变量)中 - 因此攻击者可以设置任意变量为任意值,这些值会一直保留,除非 PHP 程序显式重置它们。此外,PHP 在首次请求变量时会自动创建具有默认值的变量,因此 PHP 程序通常不会初始化变量。如果你忘记设置变量,PHP 可以报告它,但默认情况下 PHP 不会 - 请注意,这只是一个错误报告,它不会阻止发现异常方法来触发它的攻击者。因此,默认情况下,PHP 允许攻击者完全控制程序中所有变量的值,除非程序特别注意覆盖攻击者。一旦程序接管,它可以重置这些变量,但未能重置任何变量(即使是不明显的变量)可能会在 PHP 程序中打开一个漏洞。
例如,以下 PHP 程序(来自 Clowes 的示例)旨在仅让知道密码的人获取一些重要信息,但攻击者可以在他们的网络浏览器中设置“auth”并破坏授权检查
<?php if ($pass == "hello") $auth = 1; ... if ($auth == 1) echo "some important information"; ?> |
我和许多其他人抱怨过这个特别危险的问题;这是一个特别的问题,因为 PHP 被广泛使用。一种本应易于使用的语言最好能使其易于编写安全程序,毕竟。可以通过将设置“register_globals”设置为“off”来禁用 PHP 中的此错误功能,但默认情况下,PHP 4.1.0 及更早版本将此设置为“on”,并且在 register_globals 关闭的情况下,PHP 4.1.0 之前的版本更难使用。PHP 开发人员在其 PHP 4.1.0 公告中警告说,“从 PHP 的下一个半主要版本开始,PHP 的新安装将默认将 register_globals 设置为 off。” 现在已经发生了;从 PHP 4.2.0 版本开始,外部变量(来自环境、HTTP 请求、cookie 或 Web 服务器)默认不再在全局作用域中注册。访问这些外部变量的首选方法是使用 PHP 4.1.0 中引入的新超级全局数组。
对于重要的程序来说,将“register_globals”设置为“on”的 PHP 是一个危险的选择 - 它太容易编写不安全的程序了。但是,一旦将“register_globals”设置为“off”,PHP 就成为一种相当合理的开发语言。
安全的默认设置应包括将“register_globals”设置为“off”,并包括几个功能,以便用户更容易指定和限制他们将接受来自外部源的输入。然后 Web 服务器(例如 Apache)可以单独配置此安全的 PHP 安装。可以将例程放在 PHP 库中,以便用户轻松列出他们想要接受的输入变量;一些函数可以检查这些变量必须具有的模式和/或变量必须强制转换成的类型。在我看来,如果你将 register_globals 设置为 on,PHP 对于安全的 Web 开发来说是一个糟糕的选择。
正如我在本书早期版本中建议的那样,PHP 已经过简单的修改,成为安全 Web 开发的合理选择。但是,请注意,PHP 没有特别好的安全漏洞记录(例如,register_globals、文件上传问题和错误报告库中的格式字符串问题);我认为在早期版本的 PHP 中,安全问题没有得到充分考虑;我也认为 PHP 开发人员现在正在强调安全性,并且这些安全问题最终正在得到解决。一个证据是 PHP 开发人员为了关闭 register_globals 所做的重大更改;这对 PHP 用户产生了重大影响,他们愿意做出这种改变是一个好兆头。不幸的是,PHP 到底有多安全尚不清楚;现在 PHP 开发人员正在认真检查其安全性问题,PHP 还没有多少记录。希望这一点很快就会变得清晰。
如果你已决定使用 PHP,以下是我的一些建议(其中许多建议基于应对 Clowes 提出的问题的方法)
将 PHP 配置选项“register_globals”设置为 off,并使用 PHP 4.2.0 或更高版本。PHP 4.1.0 添加了几个特殊数组,特别是 $_REQUEST,这使得在“register_globals”关闭时开发 PHP 软件变得更加简单。将 register_globals 关闭,这是 PHP 4.2.0 中的默认设置,完全消除了最常见的 PHP 攻击。如果你假设 register_globals 已关闭,你应该首先检查这一点(如果不是真的则停止) - 这样,安装你的程序的人会很快知道存在问题。请注意,许多第三方 PHP 应用程序无法在此设置下工作,因此很难为整个网站保持关闭状态。可以仅为某些程序关闭 register_globals。例如,对于 Apache,你可以将以下行插入到 PHP 目录中的 .htaccess 文件中(或使用 Directory 指令进一步控制它)
php_flag register_globals Off php_flag track_vars On |
如果你必须开发在 register_globals 可能为 on 的情况下运行的软件(例如,广泛部署的 PHP 应用程序),请始终设置用户未提供的值。不要依赖 PHP 默认值,也不要信任你尚未显式设置的任何变量。请注意,你必须为每个入口点(例如,每个 PHP 程序或使用 PHP 的 HTML 文件)执行此操作。最好的方法是从每个 PHP 程序开始时设置你将要使用的所有变量,即使你只是将它们重置为通常的默认值(如““或 0)。这包括包含文件中引用的全局变量,甚至包括所有库,传递地。不幸的是,这使得此建议难以做到,因为很少有开发人员真正了解和理解他们调用的所有函数可能使用的所有全局变量。一个较小的替代方案是搜索 HTTP_GET_VARS、HTTP_POST_VARS、HTTP_COOKIE_VARS 和 HTTP_POST_FILES,看看用户是否提供了数据 - 但程序员经常忘记检查所有来源,如果 PHP 添加了新的数据源会发生什么(例如,旧版本的 PHP 中没有 HTTP_POST_FILES)。当然,这只是告诉你如何在糟糕的情况下做到最好;如果你还没有注意到,请关闭 register_globals!
将错误报告级别设置为 E_ALL,并在测试期间解决它报告的所有错误。除其他外,这将抱怨未初始化的变量,这是 PHP 中的一个关键问题。无论何时开始使用 PHP,这都是一个好主意,因为这也有助于调试程序。有很多方法可以设置错误报告级别,包括在“php.ini”文件(全局)、“.htttpd.conf”文件(单主机)、“.htaccess”文件(多主机)或通过 error_reporting 函数在脚本顶部。我建议在 php.ini 文件和脚本顶部都设置错误报告级别;这样,如果你 (1) 忘记在脚本顶部插入命令,或 (2) 将程序移动到另一台机器并忘记更改 php.ini 文件,你就会受到保护。因此,每个 PHP 程序都应该像这样开始
<?php error_reporting(E_ALL);?> |
仔细过滤用于创建文件名的任何用户信息,特别是要防止远程文件访问。PHP 默认带有“远程文件”功能 - 这意味着像 fopen() 这样的文件打开命令,在其他语言中只能打开本地文件,实际上可以用来从另一个站点调用 Web 或 ftp 请求。
不要使用旧式 PHP 文件上传;使用 HTTP_POST_FILES 数组和相关函数。PHP 通过将文件上传到具有特殊文件名的临时目录来支持文件上传。PHP 最初设置了一组变量来指示该文件名的位置,但由于攻击者可以控制变量名及其值,因此攻击者可以使用该能力造成极大的破坏。相反,始终使用 HTTP_POST_FILES 和相关函数来访问上传的文件。请注意,即使在这种情况下,PHP 的方法也允许攻击者将具有任意内容的文件临时上传到你那里,这本身就很危险。
仅将受保护的入口点放在文档树中;将所有其他代码(应该是大部分代码)放在文档树之外。PHP 在此主题上有一些不幸的建议历史。最初,PHP 用户应该对“包含”文件使用“.inc”(include)扩展名,但这些包含文件通常包含密码和其他信息,当请求者要求这样做时,Apache 只会将文档树中“.inc”文件的内容提供给请求者。然后开发人员给所有文件一个“.php”扩展名 - 这意味着内容不可见,但现在从未打算成为入口点的文件变成了入口点,有时会被利用。如前所述,通常的安全建议是最好的:仅将受保护的入口点(文件)放在文档树中,并将其他代码(例如,库)放在文档树之外。文档树中根本不应该有任何“.inc”文件。
避免会话机制。“会话”机制对于存储持久数据很方便,但其当前的实现存在许多问题。首先,默认情况下,会话将信息存储在临时文件中 - 因此,如果你在多主机系统上,你将自己暴露于许多攻击和泄露。即使那些目前不是多主机的人也可能稍后发现自己是多主机!你可以将此信息“绑定”到数据库而不是文件系统,但是如果多主机数据库上的其他人可以使用相同的权限访问该数据库,则问题是相同的。如果你不小心,还会存在歧义(“这是会话值还是攻击者的值?”),这是攻击者可以强制文件或密钥以他们选择的内容驻留在服务器上的另一种情况 - 一种危险的情况 - 并且攻击者甚至可以在某种程度上控制将放置此数据的文件或密钥的名称。
对于所有输入,检查它们是否与可接受性的模式匹配(与任何语言一样),然后使用类型转换将非字符串数据强制转换为它应该具有的类型。开发“辅助”函数以轻松检查和导入选定的(预期)输入列表。PHP 是弱类型的,这可能会导致麻烦。例如,如果输入数据的值为“000”,它将不等于“0”,也不是 empty()。这对于关联数组尤其重要,因为它们的索引是字符串;这意味着 $data["000"] 与 $data["0"] 不同。例如,为了确保 $bar 具有 double 类型(在确保它仅具有 double 的合法格式之后)
$bar = (double) $bar; |
要特别小心有风险的函数。这包括那些执行 PHP 代码的函数(例如,require()、include()、eval()、preg_replace())、命令执行(例如,exec()、passthru()、反引号运算符、system() 和 popen())以及打开文件的函数(例如,fopen()、readfile() 和 file())。这不是一个详尽的列表!
在适当的地方使用 magic_quotes_gpc() - 这消除了许多类型的攻击。
避免文件上传,并考虑修改 php.ini 文件以禁用它们 (file_uploads = Off)。文件上传在过去存在安全漏洞,因此在较旧的 PHP 版本中这是必要的,并且在更多经验表明它们是安全之前,删除它们也不是一件坏事。记住,总的来说,为了保护系统安全,你应该禁用或删除任何你不需要的东西。