WordPress插件开发安全排查
WordPress插件目录中的所有插件都必须在安全层面经过严格打磨,才能确保用户数据和网站稳定不受威胁。因此WordPress插件安全性是开发过程中绝对不能忽视的核心环节。本教程将系统讲解插件安全开发的关键技术,包括用户权限检查、数据验证、输入清理、输出转义,以及随机数的创建与验证。
一、用户权限检查
如果你的插件涉及用户提交数据的功能,那么用户权限检查都是必须优先实现的安全步骤。
1、用户角色与权限
构建WordPress插件安全防护的核心在于理解并利用WordPress自带的用户角色、权限体系,为不同身份的用户分配了明确的操作权限,是插件权限控制的基础。
每当用户登录WordPress时系统会根据其所属角色自动分配对应的权限。这里的 “角色”,本质上是对用户群体的分类,每个角色都预设了一组特定的操作权限。例如网站所有者通常拥有管理员角色,而内容创作者可能是编辑或作者角色;一个站点可以设置多个管理员,共同拥有最高操作权限。
而权限则是具体的权限标识,比如 “manage_options” 能力代表用户可查看、编辑并保存网站全局设置,仅默认分配给 “管理员” 角色,“编辑” 或其他低级别角色均无此权限。
2、用户等级制度
WordPress的用户角色存在明确的等级关系,角色等级越高,拥有的能力越全面,高级角色会自动继承所有低级角色的能力。
以单站点WordPress为例,管理员是等级最高的角色,一般同时拥有“订阅者”、“投稿者”、“作者”、“编辑” 这四个低级角色的全部权限。
示例:权限控制的正反对比
无权限限制的危险代码:
下面的代码在文章前端添加了删除文章链接,但未做任何权限检查。所有访问网站的人,包括未登录的访客都能点击链接将文章移入回收站,严重威胁网站内容安全。
<?php
/**
* 在前端创建一个删除文章的链接
*/
function wporg_generate_delete_link($content)
{
// 只在单文章页面运行
if (is_single() && in_the_loop() && is_main_query()) {
// 添加查询参数: 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;
}
/**
* 处理用户请求
*/
function wporg_delete_post()
{
if (isset($_GET['action']) && $_GET['action'] === 'wporg_frontend_delete') {
// 检查请求中是否有文章参数
$post_id = (isset($_GET['post'])) ? ($_GET['post']) : (null);
// 检查请求的文章是否存在
$post = get_post((int)$post_id);
if (empty($post)) {
return;
}
// 删除文章
wp_trash_post($post_id);
// 跳转到文章管理界面
$redirect = admin_url('edit.php');
wp_safe_redirect($redirect);
// 退出
die;
}
}
/**
* 添加删除文章的链接到文章内容后面
*/
add_filter('the_content', 'wporg_generate_delete_link');
/**
* WordPress 初始化时注册请求处理函数
*/
add_action('init', 'wporg_delete_post');
例如让WordPress中只有编辑以上角色的用户才拥有这个权限,只需将上述代码稍微修改一下,在显示链接时检查用户是否拥有 “edit_others_posts” 。
<?php
if (current_user_can('edit_others_posts')) {
/**
* 添加删除文章的链接到文章内容后面
*/
add_filter('the_content', 'wporg_generate_delete_link');
/**
* WordPress 初始化时注册请求处理函数
*/
add_action('init', 'wporg_delete_post');
}
二、数据验证
数据验证是指按照预设的规则对输入数据进行检测,判断其是否符合预期格式与范围,结果只有 “有效” 或 “无效” 两种,过程主要用于处理外部传入的数据,比如用户在表单中填写的内容、通过API接口获取的第三方数据等。
常见的数据验证场景:
- 检查必须字段是否为空
- 检查输入的电话号码是否只包含数字和符号
- 检查邮编是否为有效的邮政编码
- 检查数量字段是否大于 0
- 检查 Email 是否为有效的 Email 地址
注意:数据验证必须在后续业务逻辑(如数据库操作、功能执行)之前完成,无效数据应直接拦截,避免进入代码流程。
前端可通过JavaScript实现即时验证(提升用户体验),但后端必须再次验证,因为前端验证可被轻易绕过(如修改浏览器代码、使用爬虫提交数据),后端验证才是真正的安全屏障。
三种数据验证方法
在WordPress插件开发中,我们可通过以下三类工具实现数据验证,覆盖绝大多数场景:
1、PHP内置函数
适合基础的数据格式与范围检查,常用函数包括:
- isset() 和 empty() :用来检查一个变量是否存在;
- mb_strlen() 或者 strlen() :用来检查一个字符串长度是否符合要求;
- preg_match(),strpos() :用于检查字符串中是否包含某些指定的字符串;
- count() :用于检查数组中有多少元素;
- in_array() :用于检查数组中是否存在某元素。
- 2.、WordPress核心验证函数
- WordPress针对常见场景封装了专用验证函数,效率更高、兼容性更强,常用包括:
- is_email() :验证电子邮件地址是否有效;
- term_exists() :检查分类项目是否存在;
- username_exists() :检查用户名是否存在;
- validate_file() :检查输入的文件路径是不是一个真实的路径(不是检查文件是否存在)。
可在WordPress代码参考中,通过搜索 “_exists()”“validate()”“is*()” 这类命名规则找到更多场景化的验证函数。
3、自定义PHP/JavaScript函数
若遇到特殊验证需求,可自行编写验证函数。命名建议遵循语义化原则,如is_phone()(验证手机号)、is_available_coupon()(验证优惠券有效性)、is_zipcode()(验证邮编),便于后续维护。
自定义验证函数应返回布尔值(true表示验证通过,false表示失败),可直接作为if条件判断的依据,简化代码逻辑。
验证示例实战
示例 1:验证美国邮政编码
<input id=wporg_zip_code type=text maxlength=10 name=wporg_zip_code>
该字段限制最大输入长度为10字符,但无法限制输入内容,且浏览器可能不支持maxlength属性,或用户通过技术手段修改前端代码绕过限制,因此后端必须二次验证。
编写自定义验证函数:
<?php
function is_us_zip_code($zip_code) {
// 验证 1: 是否为空
if (empty($zip_code)) {
return false;
}
// 验证 2: 是否多于 10 个字符
if (strlen(trim($zip_code)) > 10) {
return false;
}
// 验证 3: 数据格式是否正确
if (!preg_match('/^\d{5}(\-?\d{4})?$/', $zip_code)) {
return false;
}
// 验证如果,返回 true
return true;
}
在处理表单数据时,先执行验证,再执行后续操作:
if (isset($_POST['wporg_zip_code']) && is_us_zip_code($_POST['wporg_zip_code'])) {
// 需要执行的操作
}
示例 2:验证文章排序参数
假设插件允许用户选择文章排序方式(如按作者、发布日期),需确保用户提交的排序参数在允许范围内,避免传入恶意值导致SQL注入或功能异常。
通过in_array()函数结合sanitize_key(),实现安全验证:
<?php $allowed_keys = ['author', 'post_author', 'date', 'post_date'];$orderby = sanitize_key($_POST['orderby']);
if (in_array($orderby, $allowed_keys, true)) {
// 修改查询,根据 order_by 键来排序
}
三、安全输入
安全输入即对用户提交的原始数据进行 “清理”,移除或转换其中可能存在的恶意内容(如HTML 标签、脚本代码)。当我们无法通过严格验证确定数据类型,或数据格式灵活(如用户留言、富文本内容)时,输入清理就成为保障安全的关键步骤。
需明确的是,输入清理与数据验证并非互斥,而是互补关系,验证确保数据 “符合规则”,清理确保数据 “无恶意内容”,两者结合才能最大化输入安全。
清理数据的最优方案:使用WordPress内置函数
WordPress封装了一系列sanitize_*()前缀的辅助函数,针对不同类型的数据场景做了优化,无需自行编写复杂过滤逻辑,直接调用即可实现安全清理。
常用的sanitize_*()函数清单:
- sanitize_email()
- sanitize_file_name()
- sanitize_html_class()
- sanitize_key()
- sanitize_meta()
- sanitize_mime_type()
- sanitize_option()
- sanitize_sql_orderby()
- sanitize_text_field()
- sanitize_title()
- sanitize_title_for_query()
- sanitize_title_with_dashes()
- sanitize_user()
- esc_url_raw()
- wp_filter_post_kses()
- wp_filter_nohtml_kses()
输入清理示例:普通文本字段处理
假设插件有一个 “标题” 输入字段,需存储到文章自定义元数据中,使用sanitize_text_field()清理输入:
<input id=title type=text name=title>
后端清理与存储:
$title = sanitize_text_field($_POST['title']); update_post_meta($post->ID, 'title', $title); sanitize_text_field() 执行以下操作:
- 检查无效的 UTF-8 字符;
- 将大于小于字符(><)转换为实体;
- 删除所有 HTML 标签;
- 删除换行符,制表符和额外的空白字符;
- 删除所有 8 字节字符。
四、安全输出
安全输出指的是在将数据渲染到网页之前,将可能被浏览器解析为代码的特殊字符(如< > ' ")转换为HTML实体或安全格式,避免浏览器将其执行为脚本。
这一步的核心目的是防范XSS攻击,攻击者可能通过注入恶意脚本(如<script>stealCookies()</script>),在其他用户访问页面时窃取Cookie、篡改内容甚至控制网站。而输出转义能让这些恶意脚本被当作普通文本显示,无法执行。
1、WordPress常用输出转义函数
esc_html():在显示 HTML 时,使用此函数;
esc_url():在输出 URL 时,使用此函数,包括在src和href属性中的 URL;
esc_js():对内联 JavaScript 使用此函数;
esc_attr():把数据设置为 HTML 元素属性时使用此能力。
2、使用本地化函数
WordPress本地化函数包括_e() 或 __()等,例如下面的本地化函数 esc_html_e 函数集成了数据转义的能力:
esc_html_e( 'Hello World', 'text_domain' ); // 等同于 echo esc_html( __( 'Hello World', 'text_domain' ) );
结合了本地化和转义能力的函数有:
- esc_html __()
- esc_html_e()
- esc_html_x()
- esc_attr__()
- esc_attr_e()
- esc_attr_x()
3、自定义转义
自定义转义通过函数wp_kses() 实现,一般适用于需要以特殊方式转义输出的情况,确保只有指定的 HTML元素、属性和属性值会出现在输出中,并对HTML实体进行规范化。
$allowed_html = [ 'a' => [ 'href' => [], 'title' => [], ], 'br' => [], 'em' => [], 'strong' => [], ]; echo wp_kses( $custom_content, $allowed_html );
wp_kses_post() 是 wp_kses 函数的封装,其中$allowed_html是显示文章内容使用的一组规则。
echo wp_kses_post( $post_content );
五、随机数验证
随机数验证的原理是通过一次性请求的方式验证来源和意图。生成链接时可以使用wp_create_nonce()函数添加一个随机数到链接,传递给该函数的参数确保创建的随机数对于该操作是唯一的。然后在处理删除请求时检查该随机数是否与期望一致。
使用能力检查、数据验证、安全输入、安全输入和随机数验证的完整示例:
<?php
/**
* 在前端创建一个删除文章的链接
*/
function wporg_generate_delete_link($content){
// 只在单文章页面运行
if (is_single() && in_the_loop() && is_main_query()) {
// 添加查询参数: action, post
$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;
}
/**
* 处理用户请求
*/
function wporg_delete_post(){
if (
isset($_GET['action']) &&
isset($_GET['nonce']) &&
$_GET['action'] === 'wporg_frontend_delete' &&
wp_verify_nonce($_GET['nonce'], 'wporg_frontend_delete')
) {
// 检查请求中是否有文章参数
$post_id = (isset($_GET['post'])) ? ($_GET['post']) : (null);
// 检查请求的文章是否存在
$post = get_post((int)$post_id);
if (empty($post)) {
return;
}
// 删除文章
wp_trash_post($post_id);
// 跳转到文章管理界面
$redirect = admin_url('edit.php');
wp_safe_redirect($redirect);
// 退出
die;
}
}
if (current_user_can('edit_others_posts')) {
/**
* 添加删除文章的链接到文章内容后面
*/
add_filter('the_content', 'wporg_generate_delete_link');
/**
* WordPress 初始化时注册请求处理函数
*/
add_action('init', 'wporg_delete_post');
}
广告合作
QQ群号:4114653
温馨提示:
- 本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。邮箱:2942802716#qq.com(#改为@)。
- 本站原创内容未经允许不得转裁,转载请注明出处“站长百科”和原文地址。