插件上线前夜,我靠这份"反向破坏"清单逼出了三个隐藏漏洞:从"假装自己是攻击者"的角度做最后一轮验收
之前发过一份正向逐项确认的 checklist,这次换个更狠的思路——上线前花 20 分钟"扮演攻击者",专门挑那些"开发者觉得不会有人这么干"的路径下手。上周用这招从一个即将上线的会员插件里逼出了三个真漏洞,分享下我的"反向破坏"清单,按权限、路由、菜单、配置四个维度展开。
权限维度:故意给错身份,看系统会不会"默认放行"
正向测试是"登录后能否访问",反向测试是"没登录/低权限用户碰到这个入口会怎样"。我整理了三组必做的"身份欺骗":
1. 直接构造 URL 访问后台配置页,不带任何 nonce 参数
curl -I 'https://site.test/wp-admin/admin.php?page=my-plugin-settings'
预期:302 到登录页或 403。如果返回 200 且页面渲染了,说明漏了 current_user_can 或把它放在了页面内容之后。
2. 用订阅者账号登录后,复制管理员看到的 AJAX action,改 user_id 参数指向其他用户
// 故意在 payload 里塞个不属于当前用户的 ID
$_POST['target_user'] = 1; // 管理员 ID
预期:即使当前用户是订阅者,服务端也必须重新校验 target_user 的归属权,不能信任客户端传来的"操作对象"。
3. 在已登录状态下直接清空 cookie 里的 wordpress_logged_in_*,刷新页面
有些插件用 is_user_logged_in() 做第一道门,但后续逻辑依赖 wp_get_current_user(),两者在边缘场景下可能不同步。
路由维度:往 URL 里塞垃圾,看路由解析会不会"破防"
自定义路由最容易在"非预期输入"下暴露问题。我的破坏清单:
1. 在 endpoint 后面加多层斜杠和乱码
/api/my-plugin/v1/users///../../../etc/passwd
观察:返回 404 还是 500?有没有暴露绝对路径或 SQL 错误?
2. 对需要数字 ID 的参数,传字符串、负数、超大数、科学计数法
/api/my-plugin/v1/user/1e10
/api/my-plugin/v1/user/-1 UNION SELECT...
重点检查:路由正则是否用了 (\d+) 这种宽松匹配,以及参数进入数据库前有没有类型强制转换。
3. 注册路由时用了 permission_callback => '__return_true' 的临时写法,上线前用 grep -r "__return_true" . 扫一遍代码库
这个我踩过两次,都是本地调试时写的,commit 时忘了改。
菜单维度:把菜单当成"信息泄露"的攻击面
WordPress 后台菜单的可见性不等于功能可用性,但菜单本身会泄露插件存在和结构信息:
1. 用低权限账号登录,检查 admin_menu 钩子是否"晚于"某些敏感页面的注册
// 错误示范:先注册了页面,再在 menu 里判断权限
add_submenu_page(...); // 这里已经暴露了 slug
if (!current_user_can('manage_options')) return;
2. 直接访问被"隐藏"的子菜单 slug
有些开发者用 CSS display:none 或 JS 移除菜单项,但页面本身没做权限拦截。用 admin.php?page=被隐藏的slug 直接访问。
3. 检查多站点环境下菜单是否按 is_network_admin() 正确分叉
is_multisite() && is_network_admin() 的组合很容易写反,导致站点管理员看到网络级配置,或反过来。
配置维度:让配置系统进入"脏状态",看恢复逻辑
配置页的脆弱性往往在"异常中断后的状态":
1. 在 update_option 和 add_settings_error 之间故意杀进程(或断点暂停后清空 opcache)
观察重启后配置是否处于"半写"状态。我的做法是:把配置拆成独立的 option key,避免一个 JSON 串里部分字段成功部分失败。
2. 并发提交两个冲突的配置表单
用两个浏览器标签页,A 改字段 X 为 1,B 改字段 X 为 2,同时点保存。
预期:后提交的覆盖前者,还是出现竞态条件导致数据混杂?WordPress 的 option 更新没有乐观锁,需要自己在关键配置上加 version 字段做 CAS(Compare-And-Swap)。
3. 配置校验通过但持久化失败时,用户看到的"成功提示"是否真实
// 危险的写法:先 echo 成功,再 update_option
echo '<div class="updated">已保存</div>';
update_option('my_config', $data); // 这里可能写失败
我的做法是:把 update_option 的返回值当判据,成功才输出提示,失败进 add_settings_error 的错误分支。但注意——如果开了对象缓存,update_option 返回 true 只代表"缓存层已更新",数据库回写可能延迟。上次被这个坑过,现在关键配置会跟一次 $wpdb->get_var("SELECT option_value...") 做最终确认。
最后:把"反向破坏"自动化
上面这些我不会每次手动跑,写了个小的 WP-CLI 命令集,上线前执行:
wp my-plugin audit --vectors=auth,route,menu,config --role=subscriber
核心思路是用 wp_set_current_user() 切换身份,用 parse_request 和 rest_api_init 的钩子拦截路由响应,批量输出状态码和是否有意外回显。不是替代人工 review,是帮我在 2 分钟内定位"最值得深入看"的薄弱点。
你们上线前有什么"故意搞破坏"的骚操作?或者被我漏掉的攻击向量?

