插件安全
清理数据
不受信任的数据来自许多来源(用户、第三方网站,甚至您自己的数据库!),所有这些数据在使用之前都需要进行检查。
请记住:即使管理员也是用户,用户也会有意或无意地输入错误的数据。你的工作就是保护他们免受伤害。
清理输入是保护/清理/过滤输入数据的过程。验证优于清理,因为验证更具体。但当“更具体”不可能时,消毒就是下一个最好的选择。
例子
假设我们有一个名为的输入字段title
:
<input id="title" type="text" name="title">
我们不能在这里使用验证,因为文本字段太笼统:它可以是任何东西。因此,我们使用以下函数清理输入数据sanitize_text_field()
:
$title = sanitize_text_field( $_POST['title'] );
update_post_meta( $post->ID, 'title', $title );
在后台,sanitize_text_field()
执行以下操作:
- 检查无效的 UTF-8
- 将单个小于号 (<) 转换为实体
- 删除所有标签
- 删除换行符、制表符和多余的空白
- 剥离八位字节
消除功能
有许多功能可以帮助您清理数据。
sanitize_email()
sanitize_file_name()
sanitize_hex_color()
sanitize_hex_color_no_hash()
sanitize_html_class()
sanitize_key()
sanitize_meta()
sanitize_mime_type()
sanitize_option()
sanitize_sql_orderby()
sanitize_term()
sanitize_term_field()
sanitize_text_field()
sanitize_textarea_field()
sanitize_title()
sanitize_title_for_query()
sanitize_title_with_dashes()
sanitize_user()
sanitize_url()
wp_kses()
wp_kses_post()
验证数据
不受信任的数据来自许多来源(用户、第三方网站,甚至您自己的数据库!),所有这些数据在使用之前都需要进行检查。
请记住:即使管理员也是用户,用户也会有意或无意地输入错误的数据。你的工作就是保护他们免受伤害。
验证输入是根据预定义模式(或多个模式)测试数据的过程,并得出明确的结果:有效或无效。与清理相比,验证是一种更具体的方法,但两者都有其作用。
简单的验证示例:
- 检查必填字段是否未留空
- 检查输入的电话号码是否仅包含数字和标点符号
- 检查请求的字符串是否是五个有效选项之一
- 检查数量字段是否大于 0
应尽早进行数据验证。这意味着在执行任何操作之前验证数据。
验证理念
关于如何进行验证有几种不同的理念。每个都适合不同的场景。
安全名单
仅接受来自已知和可信值的有限列表的数据。
将不受信任的数据与安全列表进行比较时,确保使用严格的类型检查非常重要。否则,攻击者可能会以某种方式制作输入,使其通过安全列表,但仍然具有恶意效果。
比较运算符
$untrusted_input = '1 malicious string'; // will evaluate to integer 1 during loose comparisons
if ( 1 === $untrusted_input ) { // == would have evaluated to true, but === evaluates to false
echo '<p>Valid data';
} else {
wp_die( 'Invalid data' );
}
in_array()
$untrusted_input = '1 malicious string'; // will evaluate to integer 1 during loose comparisons
$safe_values = array( 1, 5, 7 );
if ( in_array( $untrusted_input, $safe_values, true ) ) { // `true` enables strict type checking
echo '<p>Valid data';
} else {
wp_die( 'Invalid data' );
}
switch()
$untrusted_input = '1 malicious string'; // will evaluate to integer 1 during loose comparisons
switch ( true ) {
case 1 === $untrusted_input: // do your own strict comparison instead of relying on switch()'s loose comparison
echo '<p>Valid data';
break;
default:
wp_die( 'Invalid data' );
}
黑名单
拒绝来自已知不可信值的有限列表中的数据。这很少是一个好主意。
格式检测
测试数据的格式是否正确。如果是的话,只接受它。
if ( ! ctype_alnum( $data ) ) {
wp_die( "Invalid format" );
}
if ( preg_match( "/[^0-9.-]/", $data ) ) {
wp_die( "Invalid format" );
}
格式修正
接受大部分数据,但删除或更改危险部分。
$trusted_integer = (int) $untrusted_integer;
$trusted_alpha = preg_replace( '/[^a-z]/i', "", $untrusted_alpha );
$trusted_slug = sanitize_title( $untrusted_slug );
实例一
假设我们有一个输入字段旨在接受美国邮政编码:
<input type="text" id="wporg_zip_code" name="my-zipcode" maxlength="10" />
在这里,我们告诉浏览器最多只允许输入十个字符……但是对于可以输入的字符没有限制。他们可以输入11221
或eval()
。
这就是验证的用武之地。在处理表单时,我们编写代码来检查每个字段的数据类型是否正确,如果不正确则将其丢弃。
例如:要检查该my-zipcode
字段,我们可能会这样做:
/**
* Validate a US zip code.
*
* @param string $zip_code RAW zip code to check.
*
* @return bool true if valid, false otherwise.
*/
function wporg_is_valid_us_zip_code( string $zip_code ):bool {
// Scenario 1: empty.
if ( empty( $zip_code ) ) {
return false;
}
// Scenario 2: more than 10 characters.
// The `maxlength` attribute is only enforced by
// the browser, so we still need to validate the
// length of the input on the server to protect
// against a manual submission.
if ( 10 < strlen( trim( $zip_code ) ) ) {
return false;
}
// Scenario 3: incorrect format.
if ( ! preg_match( '/^d{5}(-?d{4})?$/', $zip_code ) ) {
return false;
}
// Passed successfully.
return true;
}
然后,在处理表单时,您的代码应该检查该wporg_zip_code
字段并根据结果执行操作:
if ( isset( $_POST['wporg_zip_code'] ) && wporg_is_valid_us_zip_code( $_POST['wporg_zip_code'] ) ) {
// $_POST['wporg_zip_code'] is valid; carry on
}
请注意,此特定示例正在检查提供的数据的格式是否正确;它不会检查提供的且格式正确的数据是否是有效的邮政编码。为此,您需要第二个函数来与有效邮政编码列表进行比较。
示例二
假设您的代码将查询数据库中的帖子,并且您希望允许用户对查询结果进行排序。
$allowed_keys = array( 'author', 'post_author', 'date', 'post_date' );
$orderby = sanitize_key( $_POST['orderby'] );
if ( in_array( $orderby, $allowed_keys, true ) ) {
// $orderby is valid; carry on
}
此示例代码通过将传入的排序键(存储在orderby
输入参数中)与允许的排序键数组进行比较来检查其有效性。这可以防止用户传递任意和潜在的恶意数据。
在根据数组检查传入的排序键之前,该键会被传递到内置的 WordPress 函数中 sanitize_key()
。该函数确保(除其他外)密钥为小写,这是我们想要的,因为in_array()
执行区分大小写的搜索。
传递true
到第三个参数 in_array()
可以启用严格的类型检查,这告诉函数不仅比较值,还比较值类型。这允许代码确定传入的排序键是字符串而不是其他数据类型。
验证功能
大多数验证都是作为自定义代码的一部分完成的,但也有一些辅助函数。这些是除“消毒”页面上列出的内容之外的内容。
检查WordPress 代码参考以获取更多此类功能。搜索具有如下名称的函数:*_exists()
、*_validate()
和 is_*()
。并非所有这些都是验证函数,但许多都是有帮助的。
转义数据
转义 输出是通过删除不需要的数据(例如格式错误的 HTML 或脚本标记)来保护输出数据的过程。此过程有助于在向最终用户呈现数据之前保护您的数据。
大多数 WordPress 函数都会正确准备输出数据,并且不需要额外的转义。
转义函数
WordPress 有许多辅助函数,可用于最常见的场景。
请密切注意每个函数的作用,因为有些函数会删除 HTML,而另一些函数则允许它。您必须使用最适合您所呼应的内容和上下文的功能。你总是在回声的时候想逃跑,而不是之前。
esc_html()
– 在 HTML 元素包含正在显示的数据部分时使用。这将删除 HTML。
<h4><?php echo esc_html( $title ); ?></h4>
esc_js()
– 用于内联 Javascript。
<div onclick='<?php echo esc_js( $value ); ?>' />
esc_url()
– 用于所有 URL,包括 HTML 元素的 src 和 href 属性中的 URL。
<img alt="" src="<?php echo esc_url( $media_url ); ?>" />
esc_url_raw()
– 在数据库中存储 URL 或需要非编码 URL 的其他情况下使用。esc_xml()
– 用于转义 XML 块。esc_attr()
– 用于打印到 HTML 元素属性中的所有其他内容。
<ul class="<?php echo esc_attr( $stored_class ); ?>">
esc_textarea()
– 使用它对文本进行编码以在文本区域元素内使用。wp_kses()
– 用于安全转义所有不受信任的 HTML(帖子文本、评论文本等)。这会保留 HTML。wp_kses_post()
– 替代版本wp_kses()
自动允许帖子内容中允许的所有 HTML。wp_kses_data()
– 其替代版本wp_kses()
仅允许帖子评论中允许的 HTML。
自定义转义示例
如果您需要以特定方式转义输出,函数wp_kses() (发音为“kisses”)将会派上用场。
此函数可确保输出中仅出现指定的 HTML 元素、属性和属性值,并规范化 HTML 实体。
<?php
echo wp_kses_post( $partial_html );
echo wp_kses(
$another_partial_html,
array(
'a' => array(
'href' => array(),
'title' => array(),
),
'br' => array(),
'em' => array(),
'strong' => array(),
)
); ?>
在此示例中,除 <a>
、 <br>
、 <em>
和 之外的所有标签都<strong>
将被删除。此外,如果 <a>
传递了标记,则转义可确保仅 返回thehref
和 the 。title
总是晚使用escape
最好尽可能晚地进行输出转义,最好是在输出数据时进行。
出于以下几个原因,最好晚点逃跑:
- 代码审查和部署可以更快地进行,因为一看就可以认为输出是安全的,而不是通过许多行代码来查看它在哪里以及是否已经被转义。
- 有些东西可能会无意中更改首次投射和输出之间的变量,从而引入潜在的漏洞。
- 后期转义可以更轻松地进行自动代码扫描,从而节省时间并减少审查和部署时间。
- 只要有可能,后期转义就会使代码更加健壮并且面向未来。
- 输出上的转义/转换消除了任何歧义并增加了清晰度(始终为维护者开发)。
// Okay, but not great.
$url = esc_url( $url );
$text = esc_html( $text );
echo '<a href="'. $url . '">' . $text . '</a>';
// Much better!
echo '<a href="'. esc_url( $url ) . '">' . esc_html( $text ) . '</a>';
......除非你不能
有时晚逃是不切实际的。在极少数情况下,输出无法传递到 wp_kses()
,因为根据定义,它会删除正在生成的脚本。
_escaped
在这种情况下,在创建字符串时始终进行转义,并将值存储在以, _safe
or 为后缀的变量中 _clean
(例如, $variable
变为 $variable_escaped
or $variable_safe
)。
如果一个函数无法在内部输出并且转义较晚,那么它必须始终返回“安全”HTML。这允许 不需要通过 允许此类标签echo my_custom_script_code();
的版本传递脚本标签来完成 。wp_kses()
通过本地化逃脱
通常使用 WordPress 本地化功能,例如或 ,而不是用于echo
输出数据。_e()
__()
这些函数只是将本地化函数包装在转义函数中:
esc_html_e( 'Hello World', 'text_domain' );
// Same as
echo esc_html( __( 'Hello World', 'text_domain' ) );
这些辅助函数结合了本地化和转义:
例子
echo $int;
根据它是整数还是浮点数, (int)
、 absint()
、 (float)
都是正确且可接受的。
有时, number_format()
或者 number_format_i18n()
可能更合适。
intval()
、 floatval()
是可以接受的,但是是过时的 (PHP4) 函数。
转义 HTML 属性中的任意变量
echo '<div id="', $prefix, '-box', $id, '">';
这应该通过一次调用来转义 esc_attr()
。
当变量用作属性或 url 的一部分时,最好对整个字符串进行转义,这样变量之前的潜在转义字符将被正确转义。
正确的:
echo '<div id="', esc_attr( $prefix . '-box' . $id ), '">';
错误:
echo '<div id="', esc_attr( $prefix ), '-box', esc_attr( $id ), '">';
注意: wp_create_nonce()
如果在 HTML 属性中使用,则使用创建的随机数也应该像这样转义。
在 HTML 属性中以及其他上下文中转义任意 URL
echo '<a href="', $url, '">';
这应该用 转义 esc_url()
。
正确的:
echo '<a href="', esc_url( $url ), '">';
错误:
echo '<a href="', esc_attr( $url ), '">';
echo '<a href="', esc_attr( esc_url( $url ) ), '">';
通过以下方式将任意变量传递给 JavaScript wp_localize_script()
wp_localize_script( 'handle', 'name',
array(
'prefix_nonce' => wp_create_nonce( 'plugin-name' ),
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'errorMsg' => __( 'An error occurred', 'plugin-name' ),
)
);
不需要转义,WordPress 会转义。
在 JavaScript 块中转义任意变量
<script type="text/javascript">
var myVar = <?php echo $my_var; ?>
</script>
$my_var
应该用 转义 esc_js()
。
正确的:
<script type="text/javascript">
var myVar = <?php echo esc_js( $my_var ); ?>
</script>
在内联 JavaScript 中转义任意变量
<a href="#" onclick="do_something(<?php echo $var; ?>); return false;">
$var
应该用 转义 esc_js()
。
正确的:
<a href="#" onclick="do_something(<?php echo esc_js( $var ); ?>); return false;">
转义 HTML 属性中的任意变量以供 JavaScript 使用
<a href="#" data-json="<?php echo $var; ?>">">
$var
应使用 esc_js()
, json_encode()
或 进行转义wp_json_encode()
。
正确的:
<a href="#" data-json="<?php echo esc_js( $var ); ?>">
在 HTML 文本区域中转义任意字符串
echo '<textarea>', $data, '</textarea>';
$data
应该用 转义 esc_textarea()
。
正确的:
echo '<textarea>', esc_textarea( $data ), '</textarea>';
转义 HTML 标签内的任意字符串
echo '<div>', $phrase, '</div>';
这取决于是否 $phrase
期望包含 HTML。
- 如果没有,请使用
esc_html()
或其任何变体。 - 如果需要 HTML,请使用
wp_kses_post()
,wp_kses_allowed_html()
或 以及wp_kses()
您想要允许的一组 HTML 标记。
在 XML 或 XSL 上下文中转义任意字符串
echo '<loc>', $var, '</loc>';
正确的:
echo '<loc>', ent2ncr( $var ), '</loc>';
随机数
随机数是“使用一次的数字”,有助于保护 URL 和表单免遭某些类型的滥用、恶意或其他形式的滥用。
从技术上讲,WordPress 随机数并不是严格意义上的数字;而是数字。它们是由数字和字母组成的哈希值。它们也不是只使用一次:它们的“寿命”有限,之后就会过期。在此时间段内,将为给定上下文中的给定用户生成相同的随机数。该操作的随机数对于该用户将保持不变,直到该随机数生命周期完成。
WordPress 的安全令牌被称为“随机数”(尽管与真正的随机数存在上述差异),因为它们的用途与随机数几乎相同。它们有助于防范包括 CSRF 在内的多种类型的攻击,但不能防范重放攻击,因为它们不会被检查为一次性使用。切勿依赖随机数进行身份验证、授权或访问控制。使用 保护您的函数current_user_can()
,并始终假设随机数可能会受到损害。
为什么要使用随机数?
有关使用随机数的示例,请考虑管理屏幕可能会生成这样的 URL,该 URL 会丢弃帖子编号 123
http://example.com/wp-admin/post.php?post=123&action=trash
当您访问该 URL 时,WordPress 将验证您的身份验证 cookie 信息,并且如果您被允许删除该帖子,则将继续删除它。攻击者可以利用此漏洞让您的浏览器在您不知情的情况下访问该 URL。例如,攻击者可以在第 3 方页面上制作一个伪装链接,如下所示:
<img src="http://example.com/wp-admin/post.php?post=123&action=trash" />
这将触发您的浏览器向 WordPress 发出请求,浏览器会自动附加您的身份验证 cookie,并且 WordPress 会认为这是一个有效的请求。
添加随机数可以防止这种情况发生。例如,当使用随机数时,WordPress 为用户生成的 URL 如下所示:
http://example.com/wp-admin/post.php?post=123&action=trash&_wpnonce=b192fc4204
如果有人试图在没有 WordPress 生成并提供给用户的正确随机数的情况下删除编号 123 的帖子,WordPress 将向浏览器发送“403 Forbidden”响应。
创建随机数
您可以创建一个随机数并将其添加到 URL 中的查询字符串中,也可以将其添加到表单中的隐藏字段中,或者可以通过其他方式使用它。
对于要在 AJAX 请求中使用的随机数,通常将随机数添加到隐藏字段,JavaScript 代码可以从中获取它。
请注意,随机数对于当前用户的会话是唯一的,因此如果用户异步登录或注销,页面上的任何随机数将不再有效。
为访客(非登录用户)自定义随机数
默认情况下,WordPress 核心会为具有相同用户 ID(值 0
)的访客生成相同的随机数。也就是说,它不能阻止访客遭受 CSRF 攻击。为了增强关键操作的安全性,您可以为访客开发会话机制,并挂钩 nonce_user_logged_out 过滤器,以将用户 ID 值替换 0
为会话机制中的另一个随机 ID。
将随机数添加到 URL
要将随机数添加到 URL,请调用wp_nonce_url()
指定裸 URL 和表示操作的字符串。例如:
$complete_url = wp_nonce_url( $bare_url, 'trash-post_'.$post->ID );
为了获得最大程度的保护,请确保表示操作的字符串尽可能具体。
默认情况下,wp_nonce_url()
添加一个名为 的字段_wpnonce
。您可以在函数调用中指定不同的名称。例如:
$complete_url = wp_nonce_url( $bare_url, 'trash-post_'.$post->ID, 'my_nonce' );
向表单添加随机数
要将随机数添加到表单,请调用wp_nonce_field()
指定表示操作的字符串。默认情况下wp_nonce_field()
生成两个隐藏字段,一个的值为随机数,另一个的值为当前 URL(引荐来源网址),并且它会回显结果。例如,这个调用:
wp_nonce_field( 'delete-comment_'.$comment_id );
可能会回显类似的内容:
为了获得最大程度的保护,请确保表示操作的字符串尽可能具体。
您可以为 nonce 字段指定不同的名称,您可以指定您不需要引用字段,并且您可以指定您希望返回结果而不是回显。有关语法的详细信息,请参阅:wp_nonce_field()
。
创建一个随机数以其他方式使用
要创建以其他方式使用的随机数,请调用wp_create_nonce()
指定表示操作的字符串。例如:
$nonce = wp_create_nonce( 'my-action_'.$post->ID );
这只是返回随机数本身。例如:295a686963
为了获得最大程度的保护,请确保表示操作的字符串尽可能具体。
验证随机数
您可以验证在 URL、管理屏幕中的表单、AJAX 请求或其他上下文中传递的随机数。
验证从管理屏幕传递的随机数
要验证在管理屏幕中的 URL 或表单中传递的随机数,请调用check_admin_referer()
指定表示操作的字符串。
例如:
check_admin_referer( 'delete-comment_'.$comment_id );
此调用检查随机数和引荐来源网址,如果检查失败,则采取正常操作(使用“403 Forbidden”响应和错误消息终止脚本执行)。
如果您在创建随机数时未使用默认字段名称 ( _wpnonce
),请指定字段名称。
例如:
check_admin_referer( 'delete-comment_'.$comment_id, 'my_nonce' );
验证 AJAX 请求中传递的随机数
要验证 AJAX 请求中传递的随机数,请调用check_ajax_referer() 并指定表示操作的字符串。例如:
check_ajax_referer( 'process-comment' );
此调用检查随机数(但不检查引用者),如果检查失败,则默认情况下它会终止脚本执行。
如果您在创建随机数时未使用默认字段名称(_wpnonce
或)之一,或者如果您想采取其他操作而不是终止执行,则可以指定其他参数。_ajax_nonce
详细信息请参见:check_ajax_referer()
。
验证在其他上下文中传递的随机数
要验证在其他上下文中传递的随机数,请wp_verify_nonce()
调用指定随机数和表示操作的字符串。
例如:
wp_verify_nonce( $_REQUEST['my_nonce'], 'process-comment'.$comment_id );
如果结果为 false,则不再继续处理该请求。相反,采取一些适当的行动。通常的操作是调用wp_nonce_ays()
,它会向浏览器发送“403 Forbidden”响应。
修改nonce系统
您可以通过添加各种操作和过滤器来修改随机数系统。
修改随机数生存期
默认情况下,随机数的生命周期为一天。此后,即使随机数与操作字符串匹配,随机数也不再有效。要更改生命周期,请添加一个 nonce_life 过滤器,指定生命周期(以秒为单位)。
例如,要将生命周期更改为四小时:
add_filter( 'nonce_life', function () { return 4 * HOUR_IN_SECONDS; } );
执行额外验证
check_admin_referrer()
要在发现随机数和引荐来源网址有效时执行附加验证,请添加check_admin_referer
操作。
例如:
function wporg_additional_check ( $action, $result ) {
...
}
add_action( 'check_admin_referer', 'wporg_additional_check', 10, 2 );
以同样的方式check_ajax_referer()
添加一个动作。check_ajax_referer
更改错误消息
您可以使用翻译系统更改随机数无效时发送的错误消息。例如:
function my_nonce_message ($translation) {
if ($translation === 'Are you sure you want to do this?') {
return 'No! No! No!';
}
return $translation;
}
add_filter('gettext', 'my_nonce_message');
附加信息
本节包含有关 WordPress 中的随机数系统的其他信息,这些信息有时可能有用。
随机数生命周期
请注意,正如 WordPress 随机数不是“使用一次的数字”一样,随机数生命周期也不是真正的随机数生命周期。WordPress 使用具有两个刻度(生命周期的一半)的系统,并验证当前刻度和最后一个刻度的随机数。在默认设置(24 小时生命周期)中,这意味着随机数中的时间信息与自 Unix 纪元以来经过了多少个 12 小时时间段相关。这意味着在中午和午夜之间创建的随机数将具有生命周期,直到第二天中午。因此,实际寿命在 12 至 24 小时之间变化。
当随机数有效时,验证随机数的函数将返回当前刻度编号 1 或 2。例如,您可以使用此信息来刷新第二个刻度中的随机数,以便它们不会过期。
随机数安全性
如果您正确安装了 WordPress,则使用您网站特有的密钥和盐生成随机数。NONCE_KEY
和NONCE_SALT
在您的文件中定义wp-config.php
,并且该文件包含提供更多信息的注释。
切勿依赖随机数进行身份验证或授权或访问控制。使用 保护您的函数current_user_can()
,始终假设随机数可能会受到损害。
更换随机数系统
构成随机数系统的一些函数是可插入的,以便您可以通过提供自己的函数来替换它们。
要更改管理请求或 AJAX 请求的验证方式,您可以替换check_admin_referrer()
或check_ajax_referrer()
,或两者都替换。
要将随机数系统替换为其他随机数系统,您可以替换wp_create_nonce()
,wp_verify_nonce()
和wp_nonce_tick()
。
有关的
随机数函数:wp_nonce_ays()
, wp_nonce_field()
, wp_nonce_url()
, wp_verify_nonce()
, wp_create_nonce()
, check_admin_referer()
, check_ajax_referer()
,wp_referer_field()
随机数挂钩:nonce_life
, nonce_user_logged_out
, explain_nonce_(verb)-(noun)
,check_admin_referer
用户角色和能力
用户角色和能力
如果您的插件允许用户提交数据(无论是在管理员端还是公共端),它应该检查用户功能。
用户角色和能力
创建高效安全层的最重要步骤是建立用户权限系统。WordPress 以用户角色和功能的形式提供这一点。
每个登录 WordPress 的用户都会根据其用户角色自动分配特定的用户功能。
用户角色只是一种表示用户属于哪个组的奇特方式。每个组都有一组特定的预定义功能。
例如,网站的主要用户将具有管理员的用户角色,而其他用户可能具有编辑者或作者等角色。您可以为一个角色分配多个用户,即一个网站可能有两个管理员。
用户能力是您分配给每个用户或用户角色的特定权限。
例如,管理员具有“manage_options”功能,允许他们查看、编辑和保存网站的选项。另一方面,编辑者缺乏这种能力,这将阻止他们与选项进行交互。
然后在管理中的各个点检查这些功能。取决于分配给角色的能力;菜单、功能和 WordPress 体验的其他方面可能会被添加或删除。
当您构建插件时,请确保仅在当前用户具有必要的功能时才运行您的代码。
等级制度
用户角色越高,用户拥有的能力越多。每个用户角色都会继承层次结构中先前的角色。
例如,“管理员”是单个站点安装中的最高用户角色,它继承以下角色及其功能:“订阅者”、“贡献者”、“作者”和“编辑者”。
例子
无限制
下面的示例在前端创建一个链接,该链接提供了垃圾帖子的功能。因为此代码不检查用户能力,所以它允许该网站的任何访问者删除帖子!
/**
* Generate a Delete link based on the homepage url.
*
* @param string $content Existing content.
*
* @return string|null
*/
function wporg_generate_delete_link( $content ) {
// Run only for single post page.
if ( is_single() && in_the_loop() && is_main_query() ) {
// Add query arguments: action, post.
$url = add_query_arg(
[
'action' => 'wporg_frontend_delete',
'post' => get_the_ID(),
], home_url()
);
return $content . ' <a href="' . esc_url( $url ) . '">' . esc_html__( 'Delete Post', 'wporg' ) . '</a>';
}
return null;
}
/**
* Request handler
*/
function wporg_delete_post() {
if ( isset( $_GET['action'] ) && 'wporg_frontend_delete' === $_GET['action'] ) {
// Verify we have a post id.
$post_id = ( isset( $_GET['post'] ) ) ? ( $_GET['post'] ) : ( null );
// Verify there is a post with such a number.
$post = get_post( (int) $post_id );
if ( empty( $post ) ) {
return;
}
// Delete the post.
wp_trash_post( $post_id );
// Redirect to admin page.
$redirect = admin_url( 'edit.php' );
wp_safe_redirect( $redirect );
// We are done.
die;
}
}
/**
* Add the delete link to the end of the post content.
*/
add_filter( 'the_content', 'wporg_generate_delete_link' );
/**
* Register our request handler with the init hook.
*/
add_action( 'init', 'wporg_delete_post' );
受限于特定能力
上面的示例允许该网站的任何访问者单击“删除”链接并删除该帖子。但是,我们只希望编辑及以上级别能够单击“删除”链接。
为了实现这一点,我们将检查当前用户是否具有该能力edit_others_posts
,只有编辑者或以上人员才具有:
/**
* Generate a Delete link based on the homepage url.
*
* @param string $content Existing content.
*
* @return string|null
*/
function wporg_generate_delete_link( $content ) {
// Run only for single post page.
if ( is_single() && in_the_loop() && is_main_query() ) {
// Add query arguments: action, post.
$url = add_query_arg(
[
'action' => 'wporg_frontend_delete',
'post' => get_the_ID(),
], home_url()
);
return $content . ' <a href="' . esc_url( $url ) . '">' . esc_html__( 'Delete Post', 'wporg' ) . '</a>';
}
return null;
}
/**
* Request handler
*/
function wporg_delete_post() {
if ( isset( $_GET['action'] ) && 'wporg_frontend_delete' === $_GET['action'] ) {
// Verify we have a post id.
$post_id = ( isset( $_GET['post'] ) ) ? ( $_GET['post'] ) : ( null );
// Verify there is a post with such a number.
$post = get_post( (int) $post_id );
if ( empty( $post ) ) {
return;
}
// Delete the post.
wp_trash_post( $post_id );
// Redirect to admin page.
$redirect = admin_url( 'edit.php' );
wp_safe_redirect( $redirect );
// We are done.
die;
}
}
/**
* Add delete post ability
*/
add_action('plugins_loaded', 'wporg_add_delete_post_ability');
function wporg_add_delete_post_ability() {
if ( current_user_can( 'edit_others_posts' ) ) {
/**
* Add the delete link to the end of the post content.
*/
add_filter( 'the_content', 'wporg_generate_delete_link' );
/**
* Register our request handler with the init hook.
*/
add_action( 'init', 'wporg_delete_post' );
}
}
常见漏洞
常见漏洞
安全是一个不断变化的环境,漏洞会随着时间的推移而演变。以下讨论了您应该防范的常见漏洞,以及保护您的主题免遭利用的技术。
漏洞类型
SQL注入
当输入的值未正确清理时,就会发生 SQL 注入,从而可能执行输入数据中的任何 SQL 命令。为了防止这种情况,WordPress API 非常丰富,提供了诸如此类的功能, add_post_meta();
而无需您通过 SQL ( INSERT INTO wp_postmeta…
) 手动添加帖子元。

xkcd妈妈的功绩
强化主题抵御 SQL 注入的第一条规则是:当有 WordPress 功能时,使用它。
但有时您需要执行 API 中未考虑到的复杂查询。如果是这种情况,请始终使用这些 $wpdb
功能。这些是专门为保护您的数据库而构建的。
SQL查询中的所有数据在执行SQL查询之前都必须进行SQL转义,以防止SQL注入攻击。用于 SQL 转义的最佳函数是$wpdb->prepare()
同时支持类似sprintf()和类似vsprintf()的语法。
$wpdb->get_var( $wpdb->prepare(
"SELECT something FROM table WHERE foo = %s and status = %d",
$name, // an unescaped string (function will do the sanitization for you)
$status // an untrusted integer (function will do the sanitization for you)
) );
跨站脚本 (XSS)
当不法分子将 JavaScript 注入网页时,就会发生跨站脚本攻击 (XSS)。
通过转义输出、删除不需要的数据来避免 XSS 漏洞。由于主题的主要职责是输出内容,因此主题应 根据内容的类型使用适当的功能转义动态内容。
转义功能之一的示例是从用户配置文件中转义 URL。
<img src="<?php echo esc_url( $great_user_picture_url ); ?>" />
可以对包含 HTML 实体的内容进行清理,以仅允许指定的 HTML 元素。
$allowed_html = array(
'a' => array(
'href' => array(),
'title' => array()
),
'br' => array(),
'em' => array(),
'strong' => array(),
);
echo wp_kses( $custom_content, $allowed_html );
跨站请求伪造 (CSRF)
跨站点请求伪造或 CSRF(发音为 sea-surf)是指不法分子诱骗用户在其进行身份验证的 Web 应用程序中执行不需要的操作。例如,网络钓鱼电子邮件可能包含指向某个页面的链接,该页面在 WordPress 管理员中删除用户帐户。
如果您的主题包含任何基于 HTML 或 HTTP 的表单提交,请使用 随机数 来保证用户打算执行操作。
<form method="post">
<!-- some inputs here … -->
<?php wp_nonce_field( 'name_of_my_action', 'name_of_nonce_field' ); ?>
</form>
保持最新状态
及时了解潜在的安全漏洞非常重要。以下资源提供了一个良好的起点:
例子
例子
使用功能检查、数据验证、安全输入、安全输出和随机数的完整示例:
/**
* Generate a Delete link based on the homepage url.
*
* @param string $content Existing content.
*
* @return string|null
*/
function wporg_generate_delete_link( $content ) {
// Run only for single post page.
if ( is_single() && in_the_loop() && is_main_query() ) {
// Add query arguments: action, post, nonce
$url = add_query_arg(
[
'action' => 'wporg_frontend_delete',
'post' => get_the_ID(),
'nonce' => wp_create_nonce( 'wporg_frontend_delete' ),
], home_url()
);
return $content . ' <a href="' . esc_url( $url ) . '">' . esc_html__( 'Delete Post', 'wporg' ) . '</a>';
}
return null;
}
/**
* Request handler
*/
function wporg_delete_post() {
if ( isset( $_GET['action'] )
&& isset( $_GET['nonce'] )
&& 'wporg_frontend_delete' === $_GET['action']
&& wp_verify_nonce( $_GET['nonce'], 'wporg_frontend_delete' ) ) {
// Verify we have a post id.
$post_id = ( isset( $_GET['post'] ) ) ? ( $_GET['post'] ) : ( null );
// Verify there is a post with such a number.
$post = get_post( (int) $post_id );
if ( empty( $post ) ) {
return;
}
// Delete the post.
wp_trash_post( $post_id );
// Redirect to admin page.
$redirect = admin_url( 'edit.php' );
wp_safe_redirect( $redirect );
// We are done.
die;
}
}
/**
* Add delete post ability
*/
add_action('plugins_loaded', 'wporg_add_delete_post_ability');
function wporg_add_delete_post_ability() {
if ( current_user_can( 'edit_others_posts' ) ) {
/**
* Add the delete link to the end of the post content.
*/
add_filter( 'the_content', 'wporg_generate_delete_link' );
/**
* Register our request handler with the init hook.
*/
add_action( 'init', 'wporg_delete_post' );
}
}