真实世界的 PHP 安全

作者:Xavier Spriet

在过去的两年里,PHP 核心开发者在为 PHP 用户社区提供强大的技术方面做得非常出色,这些技术在许多环境中都表现出色。随着 Web 应用程序越来越受欢迎,Web 开发者必须面对越来越多的可能存在的安全漏洞,这些漏洞有可能严重危害他们的工作。随着新技术的开发,许多教程、书籍和文章也相继出版。然而,这些新出现的威胁并没有得到应有的重视。

本文旨在为必须为其用户或客户提供高水平安全性的专业和开源 PHP 开发者而编写。本文的目的不是为开发者提供问答式的方法,而是帮助开发者在设计过程中识别他们自己的应用程序中可能存在的安全问题。从长远来看,这个过程使您,PHP 开发者,能够相应地应对新的安全威胁。

许多文章都涵盖了 PHP 安全开发的主题,并且每篇文章通常都会涵盖相同的主题。在这里,我将快速回顾一下这些基本概念,因为它们很重要,但我假设您已经熟悉这些材料,因此我不会在这上面花费太多时间。

register_globals

PHP 为用户提供了一个名为 register_globals 的配置指令,当启用该指令时,它会将应用程序中的每个变量都放在全局作用域中。这意味着作为 POST、GET、cookies 和 session 传递给 Web 服务器的变量都放在同一个“篮子”中,为开发者提供了一种方便的方式来检索这些值。

从设计上讲,启用此指令可能会影响应用程序的整体安全性,因为用户可以直接访问您可能在应用程序中使用的任何变量的内容。现在,PHP 在默认情况下禁用 register_globals,我强烈建议为了安全起见将其保持在该设置。例外情况是您的服务器还托管了假定此指令已启用的旧版应用程序。

跨站脚本攻击

跨站脚本攻击 (XSS) 是一种流行的技术,它允许用户控制 Web 应用程序的布局、内容以及整体可靠性和安全性。PHP 并非唯一容易受到此技术攻击的技术,主要是因为它实际上不是该语言的缺陷。相反,它更像是一个与 Web 应用程序总体设计相关的概念。

跨站脚本攻击以多种不同的形式存在,但一种流行的方法是在表单字段中注入 HTML 或 JavaScript 代码,以使您的应用程序显示原本不应显示的内容。这个概念说明了始终过滤应用程序的任何类型输入的重要性,无论它来自用户、另一个站点甚至来自数据库。PHP 函数 htmlentities() 通常是防止此类攻击的好方法。

GET 变量

能够为用户提供一个 URL,他们可以使用该 URL 在以后返回到他们所在的位置,这对于大多数 Web 应用程序至关重要。但是作为开发者,能够确定用户应该能够以任何可能的方式访问哪些信息非常重要。通过操纵查询字符串的内容,用户可以修改应用程序使用的变量的内容。

防止此类事件发生比仅仅过滤输入要复杂得多,但这仍然是朝着正确方向迈出的一步。或许保护您的应用程序免受此类攻击最可靠的方法是为您的应用程序建立健全的数据流方案和可靠的错误控制系统。

SQL 注入

这种对 Web 应用程序的恶意攻击可能会产生灾难性的后果,这些后果超出了大多数其他攻击(例如跨站脚本攻击)的范围,因为它有可能永久且完全地破坏您的数据库及其内容。

SQL 注入的概念非常简单。大多数 Web 应用程序接受来自 POST 和 GET 变量以及 cookies 的参数作为输入。此输入通常在 SQL 查询中用作参数,从而为用户提供动态内容。如果用户对您的数据库外观有任何了解,那么从技术上讲,他们应该能够更改您用于将 SQL 命令注入到查询中的参数。

让我们看一个简单的例子。您的应用程序接受来自表单的 POST 数据。目标是从数据库中显示 x 条记录,用户可以修改 x 以满足他们的需求。因此,您的表单只有一个名为 NUM 的字段,该字段为您的脚本提供该值。清单 1 说明了这个过程。在这种情况下,用户可以伪造一个 HTML 表单,该表单将发送一个精心构造的值,从而清空您的表。

清单 1. 基于 POST 变量在 PHP 中构建 SQL 查询

<?php
$query = "SELECT id, name FROM `records` LIMIT "
         . $_POST['NUM'];
$result = $db->select($query);
?>

清单 2. 用于执行 SQL 注入攻击的恶意表单

<form action="example.com/form.php" method="POST">
<input type="text" name="NUM"
        value="5; DELETE FROM `records`">
<input type="submit">
</form>

如果用户决定创建一个如清单 2 所示的表单,您的最终结果将如下所示

SELECT id, name FROM `records` LIMIT 5;
DELETE FROM `records`

显然,有简单的方法可以抵御此类攻击,但我注意到,大量应用程序没有保护自己免受此类攻击的功能。

在我们特定的示例中,调用 intval() 函数将 NUM 转换为整数将为防止 SQL 注入提供相当程度的安全性。但是,重要的是要理解,开发者不可能考虑到其所有 SQL 查询中使用的每个参数。因此,您真正需要做的是简化应用程序中的此过程。

由于现代基于 Web 的应用程序通常倾向于向核心模块或某种集中式交换机系统靠拢,因此在应用程序层面实施这种功能变得容易。本文稍后将介绍应用程序的简化功能的实施细节。现在,请注意以下快速提示,这些提示将帮助您构建自己的解决方案

  1. 使用正则表达式过滤 SQL 命令:如果您打算接受来自用户的文本,则此方法不适用,但它通过过滤掉 SQL 关键字来很好地阻止 SQL 注入(清单 3)。

  2. 使用断言:断言将在本文稍后详细介绍。

  3. 转义字符串:如果您不希望接受二进制数据作为输入,那么保护输入的一个重要步骤是使用字符串转义。在上面的示例中,转义字符串不会有帮助;但是,许多 SQL 注入攻击都基于过早退出 SQL 查询并在其中注入新查询。通过使用诸如 mysql_escape_string() 之类的函数可以有效地防止这种情况。

清单 3. 简单的“有害 SQL 命令”过滤器

<?php
function filter_sql($input) {
    $reg = "(delete)|(update)|(union)|(insert)";
    return(eregi_replace($reg, "", $input));
}
?>

加密

敏感信息通常存储在数据库服务器和其他存储设施中,以供以后检索。此时,至关重要的是要拥有一个工具,使您作为开发者能够在存储时保护该数据,并在需要时检索您正在查找的信息。

PHP 提供了一个扩展,允许开发者使用 Mcrypt 库 (mcrypt.sf.net) 通过加密数据并在以后解密数据来保护数据。PHP 的 Mcrypt 扩展的文档位于 www.php.net/mcrypt,在实施之前应仔细研究。

Mcrypt 扩展支持令人印象深刻的算法阵列,包括 triple-DES、Blowfish、Twofish 和 Two-Way。如果您不熟悉加密,则使用 Mcrypt 扩展不是一个非常直观的过程;由于可用的块算法和加密模式种类繁多,它可能会变得非常令人困惑。请参阅清单 4,了解 Mcrypt 扩展提供的功能以及如何使用它的示例。

清单 4. Mcrypt 扩展的典型用法

<?php
/* Create your key at random
   but keep it handy as you
   will use it to decrypt later
*/
$key = "AOQKJLCLIGAKJHSD
        <NKLXASLUIHJKHAS
        OIUDSgfuyJKLBLKU";

$string = $_POST['password'];

/* First, you must open the encryption module
   provided by Mcrypt */
$mod = mcrypt_module_open ('blowfish','','ecb','');

/* You must then create an Initialization Vector
   based on a size and a source.
   Your source can be custom, but some constants
   are available.
   Defining the size of the vector depends on the
   module you are using */
$iv_size = mcrypt_enc_get_iv_size($mod);

/* The initialization vector will be based on $size
   characters from the source /dev/random in our
   example */
$iv = mcrypt_create_iv($iv_size,MCRYPT_DEV_RANDOM);

/* The next step is to ensure that your key is not
   too big and truncate it if necessary */
$max_key_size = mcrypt_enc_get_key_size($mod);
$key = substr($key,0,$max_key_size);

/* You must then initialize the encryption
   mechanism through mcrypt_generic_init */
mcrypt_generic_init ($mod,$key,$iv);

/* You can now encrypt your data through
   the use of mcrypt_generic. The function
   will return your encrypted data */
$encrypted = mcrypt_generic($mod,$string);

/* Once you have finished using Mcrypt, you
   must free the buffers used during the process */
mcrypt_generic_deinit ($mod);

/* Finally, you must close the encryption module
   you have used*/
mcrypt_module_close ($mod);

/* Now here is how we can decrypt our data: */
$padded = // see next line
mcrypt_decrypt('blowfish',$key,$encrypted,'ecb',$iv);
/* At this point, our decrypted string has been
   zero-padded so we need to remove the extra \0s */
$plain = str_replace("\0","",$padded);
echo "Encrypted string: $encrypted<br>";
echo "Decrypted string: $plain<br>";
?>

断言

断言为 PHP 开发者提供了一种实现错误控制和保持数据完整性的方法。这不是 PHP 的安全相关功能,它在许多主流语言(如 C 或 Python)中都已实现,那么我为什么要现在提出它呢?简而言之,错误控制是为您的用户或客户提供有效安全性的第一步。

断言在 PHP 中通过使用两个函数 assert_options() 和 assert() 来实现。前者应在应用程序的初始化或配置文件中调用,后者应在代码中任何需要强制输入有效性的地方实现。清单 5 演示了如何使用断言来创建错误控制系统,该系统在断言失败时生成简单的报告。

清单 5. 通过断言进行错误报告

<?php

/* You can toggle assertions throughout your entire
   application by switching ASSERT_ACTIVE to 1 or 0
*/
assert_options(ASSERT_ACTIVE,1);

/* We do want the application to exit when an
   assertion fails. (in this example)
*/
assert_options(ASSERT_BAIL,1);

/* In our example, we will do the error reporting
   ourselves so we turn off the default warnings
*/
assert_options(ASSERT_WARNING,0);

/* display_error will be the name of our custom
   function that will be called if an assertion
   fails
*/
assert_options(ASSERT_CALLBACK, "display_error");

$email = strtolower($_POST['email']);
$parts = array();

// Building your regular expression
$regex = "^([.\'a-z0-9]+)@([.\'a-z0-9]+)$";

/* Checking for valid format and splitting
   the email address at the same time
   Note the special formatting. Everything
   is in quotation marks and the error is
   commented. We will extract this error
   later through regular expressions.
*/
assert("ereg(\$regex, \$email, \$parts); /*
       Invalid email address: $email */");

/* This block will not be executed if the
   assertion fails so we can safely go on */
$username = $parts[1];
echo "Welcome home, " . $username;

// This is our ASSERT_CALLBACK function
function display_error($file, $line, $error) {

    // This block will extract the comment message
    $regex = "(.*)/\* (.*)\*/";
    $parts = array();
    ereg($regex, $error, $parts);
    $msg = $parts[2];

    // And we can output a nice little report
    echo "
    <table bgcolor=\"#bbbbee\">
    <tr><td colspan='2' align='center'>
    <b>Error Report</b>
    </td></tr>
    <tr><td>File:</td><td>$file</td></tr>
    <tr><td>Line:</td><td>$line</td></tr>
    <tr><td>Message:</td><td>$msg</td></tr>
    ";

}

?>

Real-World PHP Security

图 1. 清单 5 生成的示例报告

PHPUnit 项目是一个完整的单元测试套件,PHP 开发者可以免费使用,并且基于我们刚刚所做的工作。PHPUnit 的主页位于 phpunit.sf.net

数据流

如果您从事过许多不同的 Web 项目,那么您很可能已经开始使用一个通用结构来作为新项目的基础,或者您已经开发了自己的结构。在应用程序中集中管理数据的方法有很多种,并且根据定义项目的需求集,某些模型比其他模型更合适。在接下来的几段中,我将介绍一个简单的设计模板,该模板为开发者提供了足够的伸缩性和灵活性,以满足大多数企业级项目。

此时您需要做的是实施一种集中所有输入并强制其通过过滤工具的方法。这样做为您提供了以模块化方式实现附加功能所需的简单性。在我们的示例中,我们使用以下文件层次结构

  • /index.php:根目录中唯一的文件。

  • /lib:库,受 .htaccess 保护。

  • /lib/config.inc.php:配置文件。

  • /tpl:模板,受 .htaccess 保护。

  • /doc:项目和 API 文档。

  • /images。

  • /classes:类,受 .htaccess 保护。

如图 2 所示,您的应用程序的核心是 index.php 文件,它可以直接访问任何库、模板、类或配置文件,但用户永远无法访问这些文件。

Real-World PHP Security

图 2. 应用程序核心

让我们通过用户登录应用程序的示例,逐步遵循图 2 中所示的设计。

  1. 用户查询不带参数的 index.php。Index 创建一个缓冲区并将其传递给交换机,交换机调用默认模块。此模块使用模板来显示应用程序的默认页面。

  2. 用户填写身份验证表单并提交表单。表单将其输出重定向到类似如下的内容?module=account&action=login。交换机调用帐户模块的登录功能,这只是用户类的接口。该函数实例化用户类的对象。此对象是您的模块和数据库之间的接口,它执行查询。

  3. 数据从数据库发送回对象,再从对象发送回模块,模块反过来设置适当的会话变量,调用适当的模板并使用它来修改缓冲区。然后,它将响应消息发送到索引。

此特定模型中的数据流起初可能看起来有点令人困惑,但它实际上很简单。用户输入快速传递到相应的模块,并且错误控制在交换机级别实现。其他类型的输入是数据库访问和文件系统访问,它们由其相应的类过滤。每个类都扩展了一个特殊的骨架类,该类提供输入过滤功能,因此所有类都不必担心这一点。

此模型是高效的,因为它提供了可伸缩且健壮的架构,但请记住,还有许多其他有趣的模型可用。例如,您可能想查看 Phrame 项目 (phrame.sf.net),该项目提供了 Model2 方法的实现,Model2 方法是 MVC (ootips.org/mvc-pattern.html) 的衍生产品。

安全模式

无论您是 PHP 开发者还是系统管理员,都应该学习使用 PHP 的安全模式。安全模式是一组配置选项,允许系统管理员通过实施安全措施来更改 PHP 解释器的行为。从系统管理员的角度来看,这意味着您必须学习如何正确实施此功能,而不会使开发者无法在其服务器上设置其应用程序。从开发者的角度来看,您必须了解如果启用此功能,您的应用程序中可能会出现哪些问题。

如果您管理一台为 PHP 应用程序提供服务的共享服务器,并且使用此服务器的 PHP 开发者不受信任,那么启用 safe_mode 是有意义的。在 php.ini 文件中启用 safe_mode 实际上会使您的任何脚本中的任何文件相关操作都无法进行,除非文件的所有者的 UID 与正在运行的脚本的 UID 相同。PHP 还允许您在启用 safe_mode 的情况下通过启用 safe_mode_gid 选项来更改此策略。在这种情况下,PHP 会检查您尝试使用的文件的 GID 而不是 UID。

不让用户执行他们想要的任何系统二进制文件也是一种良好的做法;safe_mode_exec_dir 在这里发挥作用。此宝贵的特性使您可以告诉 PHP 不要执行任何二进制执行,通过 exec() 或任何其他函数,除非该二进制文件位于 safe_mode_exec_dir 中,例如 /usr/local/php/bin。

一旦您熟悉了 PHP 在启用 safe_mode 时实施的限制,您应该能够开发出在启用此指令的服务器上运行时不会崩溃的软件。许多 ISP 使用 safe_mode。要遵循的简单准则是

  • 尝试将文件操作(无论是读取还是写入)限制为您应用程序提供的文件。

  • 不要依赖外部软件来安装或由您的脚本执行,除非您的项目仅在您的服务器上运行。

系统管理员还可以使用其他强大的工具来确保其系统的整体安全性。这些工具包括 disable_functions,它可以阻止调用指定的功能,以及诸如 open_basedir 之类的选项,这些选项将任何文件操作限制在特定目录中。

PHP 文档团队就此主题提供了大量的文献。他们还为 safe_mode 和相关函数和指令的各个方面提供了文档。

资源

Mcrypt 扩展: php.net/mcrypt

Mcrypt 项目: mcrypt.sf.net

MVC 范例: ootips.org/mvc-pattern.html

PHP 文档: php.net/manual/en

PHP 安全: www.php.net/manual/en/security.index.php

PHPUnit 项目: phpunit.sf.net

Phrame 项目: phrame.sf.net

安全模式: www.php.net/manal/en/features.safe-mode.php

Xavier Spriet 在过去四年中一直从事 PHP 软件开发。他是 eliquidMEDIA International 的首席开发者。您可以通过 xavier@wuug.org 联系 Xavier。

加载 Disqus 评论