插件上线前最后一道闸:我是如何用"权限矩阵表"堵住那个让订阅者能删全站数据的 `delete_option` 漏洞的
上周帮朋友审计一个即将上线的会员插件,在本地一切正常,测试账号也"看起来"没问题。结果我顺手用 `delete_option('site_config')` 试了下——订阅者角色直接干掉了核心配置,后台当场变砖。
复盘发现是 register_setting 的权限参数写串行了,但 WordPress 不会报错,只是"温柔地"允许了越权操作。这让我意识到,零散地检查每个 add_menu_page 的 capability 不够,得搞一张权限矩阵表做系统性拦截。
我的矩阵表长这样
不是文档,是代码里的白名单校验层:
// 定义每个配置项的最低角色要求
private $cap_matrix = [
'api_key' => 'manage_options', // 仅管理员
'webhook_url' => 'manage_options',
'default_points' => 'edit_posts', // 编辑及以上
'signup_bonus' => 'edit_posts',
'user_can_export' => false, // 禁止任何角色通过 REST 修改
];
public function sanitize_option( $value, $option ) {
$required = $this->cap_matrix[ $option ] ?? 'manage_options';
// 显式 false = 任何请求都拒绝
if ( false === $required ) {
add_settings_error( ... );
return get_option( $option ); // 回滚原值
}
if ( ! current_user_can( $required ) ) {
// 记录审计日志,同时返回旧值
do_action( 'myplugin_unauthorized_option_attempt', $option, wp_get_current_user() );
return get_option( $option );
}
return $this->run_type_sanitize( $value, $option );
}
关键点:sanitize_callback 是最后一道物理闸口,即使前端 JS、REST 权限、菜单隐藏全崩了,这里还能兜底。
路由注册常被漏掉的"暗面"
REST 路由的 permission_callback 很多人写了,但容易忽略批量端点的差异化权限。比如这个:
register_rest_route( 'myplugin/v1', '/users/(?P<id>\d+)/points', [
'methods' => 'POST',
'callback' => [ $this, 'update_points' ],
'permission_callback' => '__return_true', // 为了"方便测试"临时写的,上线忘了改!!
] );
我的做法:上线前 grep 全目录搜 __return_true 和空 permission_callback,零容忍。另外给每个路由加权限注释标签,方便矩阵表交叉核对:
/**
* @capability edit_users
* @sensitive true (涉及资金字段)
*/
public function update_points( $request ) { ... }
菜单与路由的"影子对齐"
遇到过菜单用了 manage_options,但对应 AJAX action 只判了 is_user_logged_in()。用户看不到菜单,却能直接 curl 触发接口。
现在我的 Checklist 里有这条:每个后台功能必须同时检查菜单 capability、路由 permission_callback、以及直接方法调用的 current_user_can 三重一致。不一致就是漏洞。
配置项的"自杀开关"检测
有些配置项本身危险,比如允许关闭插件核心功能、清空数据表。我在矩阵表里给这类项加了 destructive 标记,保存前弹二次确认,且要求当前会话有 fresh nonce(非页面加载时的那个,是单独申请的短时效 token):
if ( ! empty( $this->destructive_flags[ $option ] ) ) {
check_ajax_referer( 'myplugin_destructive_' . $option, 'destructive_nonce' );
// 同时发邮件通知管理员
wp_mail( get_option('admin_email'), '危险配置变更', ... );
}
我的实际 Checklist(精简版)
贴在工位上的,每次发布前打勾:
- □ grep
__return_true/ 空permission_callback/ 缺失current_user_can - □ 矩阵表覆盖所有
register_setting、add_option、update_option入口 - □ 菜单 slug 与路由 namespace 无冲突(曾遇
slug=tools吞掉 WP 原生工具页) - □ 多站点下切换
switch_to_blog测试配置隔离性 - □ 用
WP_DEBUG + Query Monitor确认无越权查询漏出敏感字段 - □ 角色插件(如 User Role Editor)创建"纯订阅者"账号走完整功能路径
- □ 破坏性操作有审计日志 + 邮件通知 + 二次 nonce
第 6 条救过我两次:有些功能依赖"编辑"才有的 meta box,但订阅者通过直接 URL 访问 post.php 时,插件没校验就暴露了操作入口。
你们上线前有没有类似的"习惯性漏网之鱼"?我目前最头疼的是第三方扩展包可能绕过我的矩阵表,正在想怎么给 apply_filters 的回调也挂权限钩子……