处理已弃用或删除的安全相关功能

任何会影响安全性的功能变更都必须引起注意。忽视这些变化不仅容易导致代码崩溃,还可能使您的网站受到潜在攻击者的攻击。在本节中,我们将介绍 PHP 8 中与安全相关的各种功能变化。让我们从过滤器开始讨论。

检查 PHP 8 流过滤器更改

PHP 的输入/输出(I/O)操作依赖于一个称为流的子系统。这种架构的一个有趣之处是可以向任何给定的流附加流过滤器。可以附加的过滤器既可以是使用 stream_filter_register() 注册的自定义流过滤器,也可以是包含在 PHP 安装中的预定义过滤器。

需要注意的一个重要变化是,在 PHP 8 中删除了所有 mcrypt.mdecrypt. 过滤器,以及 string.strip_tags 过滤器。如果不确定 PHP 安装中包含哪些过滤器,可以运行 phpinfo()stream_get_filters()

以下是在本书使用的 PHP 7 Docker 容器中运行 stream_get_filters() 的输出结果:

root@php8_tips_php7 [ /repo/ch08 ]#
php -r "print_r(stream_get_filters());"
Array (
    [0] => zlib.*
    [1] => bzip2.*
    [2] => convert.iconv.*
    [3] => mcrypt.*
    [4] => mdecrypt.*
    [5] => string.rot13
    [6] => string.toupper
    [7] => string.tolower
    [8] => string.strip_tags
    [9] => convert.*
    [10] => consumed
    [11] => dechunk
)

下面是在 PHP 8 Docker 容器中运行的相同命令:

root@php8_tips_php8 [ /repo/ch08 ]#
php -r "print_r(stream_get_filters());"
Array (
    [0] => zlib.*
    [1] => bzip2.*
    [2] => convert.iconv.*
    [3] => string.rot13
    [4] => string.toupper
    [5] => string.tolower
    [6] => convert.*
    [7] => consumed
    [8] => dechunk
)

从 PHP 8 的输出结果中可以发现,前面提到的过滤器已被全部删除。在 PHP 8 迁移后,任何使用上述三个过滤器的代码都会被破解。现在我们来看看对自定义错误处理所做的更改。

处理自定义错误处理更改

从 PHP 7.0 开始,大多数错误都会被抛出。但 PHP 引擎不知道有错误的情况除外,如内存不足、超过时间限制或发生分段故障。另一种例外情况是程序故意使用 trigger_error() 函数触发错误。

使用 trigger_error() 函数捕获错误并不是最佳做法。最佳做法是开发面向对象代码,并将其置于 try/catch 结构中。但是,如果您被指派管理的应用程序确实使用了这种做法,那么传递给自定义错误处理程序的内容就会发生变化。

在 PHP 8 之前的版本中,传递给自定义错误处理程序的第五个参数 $errorcontext 的数据是传递给函数的参数信息。在 PHP 8 中,这个参数被忽略。为了说明两者的区别,请看下图中的简单代码示例。下面是实现这一功能的步骤:

  1. 首先,我们定义一个自定义错误处理程序,如下所示:

    // /repo/ch08/php7_error_handler.php
    function handler($errno, $errstr, $errfile,
                     $errline, $errcontext = NULL) {
        echo "Number : $errno\n";
        echo "String : $errstr\n";
        echo "File : $errfile\n";
        echo "Line : $errline\n";
        if (!empty($errcontext))
            echo "Context: \n" . var_export($errcontext, TRUE);
        exit;
    }
  2. 然后,我们定义了一个触发错误、设置错误处理程序并调用该函数的函数,如下所示:

    function level1($a, $b, $c) {
        trigger_error("This is an error", E_USER_ERROR);
    }
    set_error_handler('handler');
    echo level1(TRUE, 222, 'C');

下面是在 PHP 7 中运行的输出结果:

root@php8_tips_php7 [ /repo/ch08 ]#
php php7_error_handler.php
Number : 256
String : This is an error
File : /repo/ch08/php7_error_handler.php
Line : 17
Context:
array (
    'a' => true,
    'b' => 222,
    'c' => 'C',
)

从前面的输出中可以看到,$errorcontext 提供了函数接收到的参数信息。相比之下,请看 PHP 8 的输出结果,如图所示:

root@php8_tips_php8 [ /repo/ch08 ]#
php php7_error_handler.php
Number : 256
String : This is an error
File : /repo/ch08/php7_error_handler.php
Line : 17

正如你所看到的,除了缺少进入 $errorcontext 的信息外,输出结果完全相同。现在让我们看看如何生成回溯。

处理回溯的更改

令人惊讶的是,在 PHP 8 之前,可以通过回溯来更改函数参数。之所以可以这样做,是因为 debug_backtrace()Exception::getTrace() 产生的跟踪提供了对函数参数的引用访问。

这是一种极其糟糕的做法,因为它允许程序在可能处于错误状态的情况下继续运行。此外,在查看此类代码时,也不清楚参数数据是如何提供的。因此,PHP 8 不再允许这种做法。debug_backtrace()Exception::getTrace() 仍像以前一样运行。唯一的区别是它们不再通过引用传递参数变量。

现在让我们看看 PDO 错误处理的变化。

PDO 错误处理模式默认值已更改

多年来,当使用 PDO 扩展的数据库应用程序无法产生结果时,PHP 开发新手都会感到很困惑。在许多情况下,造成这一问题的原因是一个简单的 SQL 语法错误,但却没有被报告。这是因为在 PHP 8 之前的 PHP 版本中,PDO 的默认错误模式是 PDO::ERRMODE_SILENT

SQL 错误不是 PHP 错误。因此,正常的 PHP 错误处理无法捕获此类错误。相反,PHP 开发人员必须专门将 PDO 错误模式设置为 PDO::ERRMODE_WARNINGPDO::ERRMODE_EXCEPTION。PHP 开发人员现在可以松一口气了,因为从 PHP 8 开始,PDO 的默认错误处理模式已变为 PDO::ERRMODE_EXCEPTION

在下面的示例中,PHP 7 允许错误的 SQL 语句无声地失败:

// /repo/ch08/php7_pdo_err_mode.php
$dsn = 'mysql:host=localhost;dbname=php8_tips';
$pdo = new PDO($dsn, 'php8', 'password');
$sql = 'SELEK propertyKey, hotelName FUM hotels '
    . "WARE country = 'CA'";
$stm = $pdo->query($sql);
if ($stm)
    while($hotel = $stm->fetch(PDO::FETCH_OBJ))
        echo $hotel->name . ' ' . $hotel->key . "\n";
else
    echo "No Results\n";

在 PHP 7 中,唯一的输出是 "无结果"(No Results),这既具有欺骗性又毫无帮助。它可能会让开发人员误以为没有结果,而实际上问题出在 SQL 语法错误上。

在 PHP 8 中运行的输出(如图所示)要有用得多:

root@php8_tips_php8 [ /repo/ch08 ]# php php7_pdo_err_mode.php
PHP Fatal error: Uncaught PDOException: SQLSTATE[42000]:
Syntax error or access violation: 1064 You have an error in
your SQL syntax; check the manual that corresponds to your
MariaDB server version for the right syntax to use near 'SELEK
propertyKey, hotelName FUM hotels WARE country = 'CA'' at line
1 in /repo/ch08/php7_pdo_err_mode.php:10

从前面的 PHP 8 输出中可以看出,实际问题已被清楚地识别出来。

有关此更改的更多信息,请参阅此 RFC: https://wiki.php.net/rfc/pdo_default_errmode

接下来我们检查 php.ini 中的 track_errors 指令。

检查 php.ini 中的 track_errors 设置

自 PHP 8 起,删除了 track_errors php.ini 指令。这意味着自动创建的 $php_errormsg 变量不再可用。在大多数情况下,PHP 8 之前会导致错误的任何内容现在都已转换为抛出错误消息。不过,对于 PHP 8 以前的 PHP 版本,您仍然可以使用 error_get_last() 函数来代替。

在下面的简单代码示例中,我们首先将 track_errors 指令设置为开启。然后调用 strpos(),不带任何参数,故意造成错误。然后,我们依靠 $php_errormsg 来显示真正的错误:

// /repo/ch08/php7_track_errors.php
ini_set('track_errors', 1);
@strpos();
echo $php_errormsg . "\n";
echo "OK\n";

下面是 PHP 7 的输出结果:

root@php8_tips_php7 [ /repo/ch08 ]# php php7_track_errors.php
strpos() expects at least 2 parameters, 0 given
OK

从前面的输出中可以看到,$php_errormsg 揭示了错误,代码块被允许继续。当然,在 PHP 8 中,我们不允许调用不带任何参数的 strpos()。下面是输出结果:

root@php8_tips_php8 [ /repo/ch08 ]# php php7_track_errors.php
PHP Fatal error: Uncaught ArgumentCountError: strpos() expects
at least 2 arguments, 0 given in /repo/ch08/php7_track_errors.
php:5

如您所见,PHP 8 抛出了一条错误信息。最佳做法是使用 try/catch 块捕获可能抛出的任何错误信息。也可以使用 error_get_last() 函数。下面是一个重写的示例,可以在 PHP 7 和 PHP 8 中运行(输出未显示):

// /repo/ch08/php8_track_errors.php
try {
    strpos();
    echo error_get_last()['message'];
    echo "\nOK\n";
} catch (Error $e) {
    echo $e->getMessage() . "\n";
}

现在你已经了解了 PHP 8 中被弃用或移除的 PHP 功能。本章到此结束。

总结

在本章中,你了解了已废弃和已删除的 PHP 功能。本章第一节涉及已删除的核心功能。我们解释了更改的理由,并告诉你删除本章所述功能的主要原因不仅是为了让你的代码遵循最佳实践,而且是为了让你使用更快、更高效的 PHP 8 功能。

在下一节中,你将了解到已废弃的功能。本节的主题是强调被废弃的函数、类和方法是如何导致不良实践和错误百出的代码的。此外,我们还指导你如何使用一些关键的 PHP 8 扩展中已被删除或废弃的功能。

你将学会如何查找和重写已废弃的代码,以及如何为已删除的功能开发变通方法。在本章中学到的另一项技能包括如何重构代码,使用涉及扩展的已移除功能,最后但并非最不重要的是,你还学会了如何通过重写依赖于已移除功能的代码来提高应用程序的安全性。

在下一章中,您将学习如何通过掌握最佳实践来提高 PHP 8 代码的效率和性能。