注意:可能使您的网站不安全的 PHP 和 WordPress 函数
已发表: 2022-03-10WordPress(或任何)网站的安全性是一个多方面的问题。 任何人都可以采取的确保网站安全的最重要步骤是记住,没有任何单一的流程或方法足以确保不会发生任何不良事件。 但是您可以做一些事情来提供帮助。 其中之一是在您编写的代码和您部署的其他人的代码中密切关注可能产生负面后果的功能。 在本文中,我们将准确介绍这些功能:WordPress 开发人员在使用之前应该清楚地考虑的功能。
WordPress 本身提供了一个庞大的函数库,其中一些可能很危险。 除此之外,WordPress (PHP) 开发人员还会以某些频率使用许多 PHP 函数,这些函数在使用时可能很危险。
所有这些函数都有合法和安全的用途,但它们也是可以使您的代码很容易被滥用的函数。 我们将介绍最有可能导致安全漏洞的因素,以及您必须注意它们的原因。 我们将首先在您的代码中运行不良行为者可以用来作恶的 PHP 函数,然后讨论也会引起头痛的 WordPress 特定的 PHP 函数。
需要注意的 PHP 函数
正如我们所说,PHP 包含许多易于使用的函数。 其中一些功能因人们很容易用它们做坏事而臭名昭著。 所有这些函数都有适当的用途,但要小心你如何使用它们以及你拉入项目的其他代码是如何做的。
如果您的 PHP 版本支持它们,我们将假设您已经关闭了魔术引号和注册全局变量。 它们在 PHP 5 及更高版本中默认关闭,但可以在低于 5.4 的版本中打开。 很少有主机允许它,但我认为没有包含它们的 PHP 安全文章是不完整的。
或者提到 PHP 5.6 是最后一个仍然具有持续安全支持的 5.x 版本。 至少你应该在上面。 您应该计划在 2018 年底 5.6 支持结束之前迁移到 PHP 7。
之后,您将需要处理一些有风险的功能。 从...开始…
extract
是坏消息,尤其是在$_POST
或类似的
幸运的是,使用 PHP 的extract
函数在很大程度上已经失宠。 它使用的核心是您可以在一组数据上运行它,并且它的键值对将成为您代码中的动态变量。 所以
$arr = array( 'red' => 5 ); extract($arr); echo $red; // 5
会工作。 这很酷,但如果您要提取$_GET
、 $_POST
等,这也非常危险。在这些情况下,您实际上是在自己重新创建register_globals
问题:外部攻击者可以通过添加查询字符串轻松更改您的变量值或表单域。 最好的解决方案很简单:不要使用extract
。
如果您创建自己提取的数组,则使用extract
并不是特别安全问题,在某些情况下,它可能很有用。 但它的所有用途都有让未来读者感到困惑的问题。 创建一个数组并调用extract
比仅仅声明你的变量更令人困惑。 所以我鼓励你改为手动声明,除非它完全不可行。
如果必须对用户输入使用extract
,则应始终使用EXTR_SKIP
标志。 这仍然存在混淆问题,但它消除了恶意外来者通过简单的查询字符串或 Web 表单修改来更改您的预设值的可能性。 (因为它“跳过”了已经设置的值。)
“ eval
是邪恶的”,因为任意代码很可怕
PHP 中的eval
以及几乎任何其他带有它的语言总是在这样的列表中名列前茅。 并且有充分的理由。 PHP 在 PHP.net 上的官方文档非常坦率地说:
注意: eval() 语言结构非常危险,因为它允许执行任意 PHP 代码。 因此不鼓励使用它。 如果您已仔细验证除了使用此构造之外别无选择,请特别注意不要将任何用户提供的数据传入其中,而无需事先正确验证。
eval
让程序中的任意字符串都可以像 PHP 代码一样运行。 这意味着它对于“元编程”很有用,您正在构建一个可以自己构建程序的程序。 这也是非常危险的,因为如果您允许任意源(例如网页上的文本框)立即传递给您的eval
字符串评估器,那么您突然让恶意攻击者可以轻松地执行 PHP 可以做的任何事情在你的服务器上做。 很明显,这包括连接到数据库、删除文件以及通过 SSH 连接到机器时可以做的任何其他事情。 这是不好的。
如果你必须在你的程序中使用eval
,你应该竭尽全力确保你不允许任意的用户输入被传递给它。 如果您必须允许任意用户访问eval
的输入,请通过您不让他们运行的命令的黑名单来限制他们可以做什么,或者(更好,但更难实现)白名单,这只是您的命令考虑安全。 更好的是,只允许少量特定的参数更改,例如仅经过验证的整数。
但请始终牢记 PHP 创始人 Rasmus Lerdorf 的这句话:
“如果 `eval()` 是答案,那你几乎肯定是问错了问题。”
eval
变化
除了众所周知的eval
之外,PHP 历史上还有多种其他方式支持字符串求值作为代码。 与 WordPress 开发人员最相关的两个是带有/e
修饰符的preg_replace
和create_function
。 每个都有点不同,PHP 5.5.0 之后的preg_replace
根本不起作用。 (PHP 5.5 及以下版本不再获得官方安全更新,因此最好不要使用。)
/e
正则表达式中的修饰符也是“邪恶的”
如果您运行的是 PHP 5.4.x 或更低版本,则需要留意以 e 结尾的 PHP preg_replace
调用。 这可能看起来像:
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
$html = preg_replace( '( (.*?) )e', '" " . strtoupper("$2") . " "', $html );) // or $html = preg_replace( '~ (.*?) ~e', '" " . strtoupper("$2") . " "', $html );)
PHP 提供了多种在正则表达式中“隔离”的方法,但您要注意的核心是“e”作为preg_replace
第一个参数的最后一个字符。 如果它在那里,您实际上是将整个找到的段作为参数传递给您的内联 PHP,然后对其进行eval
。 如果您让用户输入进入您的函数,这与eval
具有完全相同的问题。 可以使用preg_replace_callback
替换示例代码。 这样做的好处是你已经写出了你的函数,所以攻击者更难改变被评估的内容。 所以你可以把上面写成:
// uppercase headings $html = preg_replace_callback( '( (.*?) )', function ($m) { return " " . strtoupper($m[2]) . " "; }, $html );
// uppercase headings $html = preg_replace_callback( '( (.*?) )', function ($m) { return " " . strtoupper($m[2]) . " "; }, $html );
// uppercase headings $html = preg_replace_callback( '( (.*?) )', function ($m) { return " " . strtoupper($m[2]) . " "; }, $html );
// uppercase headings $html = preg_replace_callback( '( (.*?) )', function ($m) { return " " . strtoupper($m[2]) . " "; }, $html );
// uppercase headings $html = preg_replace_callback( '( (.*?) )', function ($m) { return " " . strtoupper($m[2]) . " "; }, $html );
让用户输入create_function
好……
PHP 也有create_function
函数。 现在在 PHP 7.2 中已弃用,但它与eval
非常相似,并且具有相同的基本缺点:它允许将字符串(第二个参数)转换为可执行的 PHP。 它也有同样的风险:如果你不小心的话,你很容易不小心让智能破解者在你的服务器上做任何事情。
如果您使用的是 5.3 以上的 PHP,则此问题比preg_replace
更容易修复。 您可以创建自己的匿名函数,而无需使用字符串作为中介。 至少在我看来,这既更安全也更易读。
assert
Is Also eval
-Like
assert
不是我看到许多 PHP 开发人员在 WordPress 或外部使用的函数。 它的目的是针对您的代码的先决条件进行非常轻量级的断言。 但它也支持eval
类型的操作。 出于这个原因,你应该像对eval
一样警惕它。 基于字符串的断言(这是为什么不好的核心)在 PHP 7.2 中也被弃用了,这意味着它在未来应该不再受到关注。
包含变量文件是一种可能允许不受控制的 PHP 执行的方法
我们已经很好地探讨了为什么eval
不好,但是像include
或require($filename'.php')
这样的东西,特别是在$filename
是从用户可控值设置的地方,同样是坏消息。 原因与eval
略有不同。 可变文件名通常用于非 WordPress PHP 应用程序中的简单 URL 到文件路由等。 但您也可能会在 WordPress 中看到它们。
问题的核心是,当您include
或require
(或include_once
或require_once
)时,您正在使脚本执行包含的文件。 它或多或少在概念上对那个文件进行了eval
,尽管我们很少这样想。
如果您已经编写了变量include
可能引入的所有文件,并考虑了这样做时会发生什么,那么您很好。 但是,如果您没有考虑过include
password.php
.php 或wp-config.php
会做什么,那就是个坏消息。 如果有人可以添加恶意文件然后执行您的include
,这也是一个坏消息(尽管此时您可能会遇到更大的问题)。
不过,解决这个问题的方法并不难:硬编码尽可能包含在内。 如果不能,请拥有一个白名单(更好)或一个可以包含的文件的黑名单。 如果白名单中的文件(即:您已经审核了添加时的作用),您就会知道自己是安全的。 如果它不在您的白名单上,您的脚本将不会包含它。 通过这个简单的调整,你就很安全了。 白名单看起来像这样:
$white_list = ['db.php', filter.php', 'condense.php'] If (in_array($white_list, $file_to_include)) { include($file_to_include); }
永远不要将用户输入传递给shell_exec
和变体
这是一个很大的。 PHP 中的shell_exec
、 system
、 exec
和反引号都允许您正在运行的代码与底层(通常是 Unix)shell 对话。 这类似于使eval
危险但加倍的原因。 加倍是因为如果你不小心让用户输入通过这里,攻击者甚至不受 PHP 的约束。
从 PHP 运行 shell 命令的能力对于开发人员来说非常有用。 但如果你让用户的输入在那里,他们就有能力偷偷地获得许多危险的权力。 所以我什至会说用户输入永远不应该传递给shell_exec
类型的函数。
如果您想实现它,我认为处理这种情况的最佳方法是为用户提供对一小组已知安全的预定义 shell 命令的访问权限。 这可能是安全的。 但即便如此,我还是会警告你要非常小心。
注意unserialize
; 它自动运行代码
在一个活的 PHP 对象上调用serialize
的核心操作,将该数据存储在某个地方,然后使用该存储的值稍后将该对象反unserialize
,这很酷。 这也是相当普遍的,但它可能是有风险的。 为什么有风险? 如果该unserialize
调用的输入不是完全安全的(比如它被存储为 cookie 而不是在您的数据库中……),攻击者可以改变对象的内部状态,从而使unserialize
调用做坏事。
与eval
问题相比,此漏洞利用更加深奥且不太可能被注意到。 但是,如果您使用 cookie 作为序列化数据的存储机制,请不要对该数据使用serialize
。 使用类似json_encode
和json_decode
的东西。 有了这两个 PHP 将永远不会自动执行任何代码。
这里的核心漏洞是当unserialize
将 sa 字符串反序列化为一个类时,它会在该类上调用魔术__wakeup
方法。 如果允许对未经验证的用户输入进行反unserialized
,则__wakeup
方法中的数据库调用或文件删除等操作可能会指向危险或不受欢迎的位置。
unserialize
与eval
漏洞不同,因为它需要对对象使用魔术方法。 攻击者不是创建自己的代码,而是被迫在对象上滥用您已经编写的方法。 对象上的__destruct
和__toString
魔术方法也是有风险的,正如这个 OWASP Wiki 页面所解释的那样。
通常,如果您不在类中使用__wakeup
、 __destruct
或__toString
方法,就可以了。 但是因为你以后可能会看到有人将它们添加到一个类中,所以最好不要让用户靠近你的调用来serialize
和反序列化,并通过像 JSON( json_encode
和unserialize
)这样的东西传递所有公共数据以供这种使用json_decode
永远不会自动执行代码。
使用file_get_contents
获取 URL 是有风险的
快速编写一些必须调用外部 URL 的 PHP 代码时,一种常见的做法是访问file_get_contents
。 它很快,很容易,但不是超级安全。
file_get_contents
的问题很微妙,但很常见的是主机有时会将 PHP 配置为甚至不允许您访问外部 URL。 这是为了保护你。
这里的问题是file_get_contents
将为您获取远程页面。 但是当它这样做时,它不会检查 HTTPS 协议连接的完整性。 这意味着您的脚本可能成为中间人攻击的受害者,这将允许攻击者将他们想要的任何内容放入您的file_get_contents
页面结果中。
这是更深奥的攻击。 但是当我编写现代(基于 Composer)的 PHP 时,为了防止它发生,我几乎总是使用 Guzzle 来包装更安全的 cURL API。 在 WordPress 中,它更容易:使用wp_remote_get
。 它的工作方式比file_get_contents
更一致,并且默认验证 SSL 连接。 (您可以将其关闭,但是,嗯,也许不要……)更好,但更令人讨厌的是wp_safe_remote_get
等。这些功能与名称中没有safe_
的功能相同,但它们会使确保沿途不会发生不安全的重定向和转发。
不要盲目相信来自filter_var
URL 验证
所以这个有点晦涩,所以感谢 Chris Weigman 在这个 WordCamp 演讲中解释它。 一般来说,PHP 的filter_var
是验证或清理数据的好方法。 (虽然,不要对你想要做什么感到困惑......)
这里的问题非常具体:如果您尝试使用它来确保 URL 是安全的,则filter_var
不会验证协议。 这通常不是问题,但如果您将用户输入传递给此方法进行验证,并且正在使用FILTER_VALIDATE_URL
,则类似javascript://comment%0aalert(1)
的内容将通过。 也就是说,这可能是一个非常好的载体,可以在你意想不到的地方进行基本的 XSS 攻击。
对于验证 URL,WordPress 的esc_url
函数将产生类似的影响,但只允许通过允许的协议。 javascript
不在默认列表中,因此可以确保您的安全。 但是,与filter_var
不同的是,它会为传递给它的不允许的协议返回一个空字符串(不是 false)。
值得关注的 WordPress 特定功能
除了核心 PHP 可能存在漏洞的函数之外,还有一些 WordPress 特定的函数可能有点棘手。 其中一些与上面列出的各种危险函数非常相似,有些则略有不同。
WordPress 使用maybe_unserialize
如果您阅读以上内容,这可能很明显。 在 WordPress 中有一个名为maybe_unserialize
的函数,正如您猜想的那样,如果需要,它会反序列化传递给它的内容。
这并没有引入任何新的漏洞,问题很简单,就像核心的反unserialize
函数一样,这个漏洞可能导致易受攻击的对象在反序列化时被利用。
如果用户是管理员, is_admin
不回答!
这个很简单,但功能名称含糊不清,所以如果你赶时间,很容易混淆或错过。 您应该始终检查尝试在 WordPress 中执行操作的用户是否具有执行该操作所需的权利和特权。 为此,您应该使用current_user_can
函数。
但是您可能会错误地认为is_admin
会告诉您当前用户是否是管理员级别的帐户,因此应该能够设置插件使用的选项。 这是个错误。 is_admin
在 WordPress 中所做的是告诉您当前页面加载是否在站点的管理端(相对于前端)。 因此,每个可以访问管理页面(如“仪表板”)的用户都可能通过此检查。 只要您记住is_admin
是关于页面类型,而不是当前用户,就可以了。
add_query_arg()
不清理 URL
这并不常见,但几年前 WordPress 生态系统出现了一波更新浪潮,因为有关此功能的公共文档不正确。 核心问题是add_query_arg
函数(及其反向remove_query_arg
)不会自动清理站点 URL,如果 URL 没有传递给它,人们认为它确实如此。 许多插件被 Codex 误导,因此使用它不安全。
他们必须做不同的核心事情:在使用之前清理调用此函数的结果。 如果你这样做了,你就真的可以免受你认为的 XSS 攻击了。 所以看起来像:
echo esc_url( add_query_arg( 'foo', 'bar' ) );
$wpdb->query()
对 SQL 注入攻击开放
如果您了解 SQL 注入,这可能看起来很愚蠢,甚至是多余的。 因为问题是您访问数据库的任何方式(例如使用 PHP 的mysqli
或 PDO 数据库驱动程序)来创建允许 SQL 注入攻击的数据库查询。
我特别调用$wpdb->query
的原因是$wpdb
wpdb 上的一些其他方法(如insert
、 delete
等)会为您处理注入攻击。 此外,如果您习惯于使用WP_Query
或类似工具进行基本的 WordPress 数据库查询,则无需考虑 SQL 注入。 这就是为什么我要大声疾呼:当您第一次尝试使用$wpdb
进行自己的查询时,以确保您了解对数据库的注入攻击是可能的。
该怎么办? 使用$wpdb->prepare()
然后$wpdb->query()
。 您还需要确保在$wpdb
的其他“获取”方法之前做好准备,例如get_row()
和get_var()
。 否则 Bobby Tables 可能会得到你。
esc_sql
也不能保护您免受 SQL 注入
对于大多数 WordPress 开发人员来说,我会说esc_sql
注册没有任何意义,但它应该注册。 正如我们刚刚提到的,在进行任何数据库查询之前,您应该使用wpdb->prepare()
。 这会让你安全。 但是,开发人员可能会使用esc_sql
来代替,这是很诱人且可以理解的。 他们可能认为它是安全的。
问题是esc_sql
没有针对 SQL 注入的强大保护。 它确实是 PHP 的add_slashes
函数的美化版本,多年来一直不鼓励您使用它来保护您的数据库。
还有更多工作要做,但这是一个重要的开始
安全性不仅仅是在代码中寻找攻击者可能滥用的功能。 例如,我们没有深入讨论验证和清理您从用户那里收到的所有数据并在将其放入网页之前将其转义的必要性(尽管我最近发表了一篇关于该主题的文章,“保护您的 WordPress站点对抗跨站点脚本攻击”)。 但是您可以并且应该将其用作更广泛的安全策略的一部分。
在您在 WordPress 中部署新插件之前进行最终检查时,将易于滥用的功能列表放在您身边是一个好主意。 但是您还需要确保您信任主机的安全性,确保您的用户拥有正确的密码等等。
关于安全性要记住的核心问题是,最薄弱的环节才是最重要的。 一个领域的良好做法会加强其他地方可能存在的弱点,但它们永远无法完全纠正它。 但要注意这些功能,你会在大多数情况下占得先机。