子菜单 `capability` 比父菜单更宽松时,WordPress 不会拦你,但会送你一张"空白页门票"
上周帮同事看一个插件的权限问题,现象特别诡异:普通编辑角色能看到后台侧边栏里挂出来的"数据报表"菜单,点进去却不是 403,而是一张干干净净的白屏——标题栏还在,WordPress 的 admin bar 也在,就是正文区域空空如也。
一开始怀疑是回调函数里提前 `exit` 或者 `die`,跟了半小时发现流程正常走完。最后定位到一组极其隐蔽的声明顺序:
// 父菜单:仅管理员可见
add_menu_page(
'数据总览',
'数据总览',
'manage_options', // ← 管理员
'my-plugin-dashboard',
'render_dashboard',
'dashicons-chart-area',
6
);
// 子菜单:编辑也能看
add_submenu_page(
'my-plugin-dashboard',
'细分报表',
'细分报表',
'edit_posts', // ← 编辑
'my-plugin-reports',
'render_reports'
);
WordPress 的菜单渲染逻辑在这里有个"半拉子"安全模型:侧边栏的可见性由子菜单自身的 `capability` 决定,所以 `edit_posts` 满足条件的用户确实能看到入口;但当你点击进去,URL 变成 `wp-admin/admin.php?page=my-plugin-reports`,WordPress 会重新校验——这次它校验的不是子菜单的 `capability`,而是当前页面顶层父菜单的 `capability`。
校验失败不会抛 403,而是走 `wp_die` 的另一种分支:输出一个只有标题、没有内容的"无权限"页,外观酷似白屏。这个行为在 `wp-admin/includes/menu.php` 的 `_wp_menu_output` 和 `user_can_access_admin_page` 之间被拆成两步执行,中间没有任何日志。
更坑的是,如果你把子菜单的 `slug` 注册成独立顶级菜单(也就是 `add_menu_page` 而不是 `add_submenu_page`),同样的 `capability` 反而能正常工作。两种写法,一字之差,安全边界完全不同。
我现在给团队定的规矩:任何插件如果子菜单权限低于父菜单,必须在回调入口手动补一道 `current_user_can`,而不是依赖框架拦截。因为 WordPress 的拦截是"可见性"和"访问权"分开判断的,中间这个缝隙足够让运营同事凌晨两点给你打语音。
另外发现一个副产品:如果父菜单的 `capability` 填的是自定义权限节点(比如 `my_plugin_view_dashboard`),但没有通过 `map_meta_cap` 做映射,超级管理员也会进白屏——因为 `user_can_access_admin_page` 里走的是 `current_user_can( $parent[1] )`,而自定义节点默认对任何人返回 false,包括 `administrator`。
你们有遇到过这种"不报错、不 403、就是空白"的权限场景吗?我怀疑还有不少插件在依赖这个"半拉子"机制当安全边界用。