多站点环境下插件菜单"幽灵显示":我是如何用 `is_network_admin` 与 `is_blog_admin` 的误判,把超级管理员配置页漏给站点管理员的

插件开发 17 浏览 0 回复 返回上级

上周给多站点环境打包一个"全局流量监控"插件,本地单站测试一切正常,扔到 staging 的 multisite 一测,站点管理员(非超级管理员)居然能在子站后台看到"网络级设置"入口,点进去还能改全局阈值——当场冷汗。

问题不在 `current_user_can`,在菜单注册的位置判断。我原先是这么写的:

if ( current_user_can( 'manage_network_options' ) ) {
    add_menu_page(
        '全局流量监控',
        '流量监控',
        'manage_network_options', // capability
        'netflow-dashboard',
        'render_dashboard',
        'dashicons-chart-area',
        3
    );
}

看起来没毛病?但这段代码我塞在 admin_menu 钩子里,没区分网络后台还是站点后台。WordPress 的多站点有个坑:admin_menu两个上下文都会触发,而 `manage_network_options` 在子站后台的 `map_meta_cap` 里会被映射成 `do_not_allow`——但映射前,add_menu_page 已经执行完了。

更骚的是,WordPress 的菜单渲染层有个"静默降级":如果当前用户没有某个 capability,但菜单已经注册了,它不会报错,而是把菜单项显示出来,只是链接可能带 `?page=netflow-dashboard` 然后进去再 403。但我的页面渲染函数里又加了一层权限检查,所以最终是 403 白页——用户体验极差,而且暴露了功能入口。

正确的姿势是双保险:位置判断 + capability 过滤 + 渲染兜底。我现在的上线 checklist 里这一项长这样:

// 1. 先卡位置:网络级菜单只在网络后台注册
if ( ! is_network_admin() ) {
    return;
}

// 2. 再卡权限:虽然上面已经隔离,但防御性保留
if ( ! current_user_can( 'manage_network_options' ) ) {
    return;
}

// 3. 注册时显式声明网络菜单
add_menu_page(
    '全局流量监控',
    '流量监控',
    'manage_network_options',
    'netflow-dashboard',
    'render_dashboard',
    'dashicons-chart-area',
    3
);

// 4. 渲染函数里再兜底(防止直接 URL 访问)
function render_dashboard() {
    if ( ! current_user_can( 'manage_network_options' ) ) {
        wp_die( '需要网络管理权限' );
    }
    // ...
}

但这里还有个隐藏坑:is_network_admin()admin_init 之前不可靠,因为 current_screen 还没初始化。所以如果你把菜单注册挂在 admin_init 而不是 network_admin_menu,判断会失效。

我现在的做法是把网络级菜单单独挂 network_admin_menu,站点级菜单挂 admin_menu,彻底物理隔离:

add_action( 'network_admin_menu', 'register_network_menus' );
add_action( 'admin_menu', 'register_site_menus' );

最后贴一下我插件上线前针对"菜单/路由/权限"的 checklist 精简版,专门给多站点场景:

  • 位置隔离:网络菜单是否挂在 `network_admin_menu`?站点菜单是否确认不会在网络后台泄漏?
  • Capability 穿透测试:用 `wp_set_current_user()` 切换订阅者/作者/编辑/站点管理员/超级管理员,逐角色扫菜单可见性
  • 直接 URL 访问:把 `?page=xxx` 链接发给低权限账号,看是否能绕过菜单层直达页面
  • 子站切换测试:在 A 子站后台登录,改 URL 里的 `blog_id` 去 B 子站,看菜单状态是否同步错乱
  • 路由冲突扫描:多站点下 `menu_slug` 全局唯一吗?两个子站插件如果 slug 撞车,网络菜单会吞掉后注册的

第 5 条是我踩的新坑:两个不同插件都用了 `global-stats` 当 slug,结果网络后台只显示一个,另一个的回调被覆盖,但 capability 检查还是按后注册的来,导致先注册的页面用了错误的权限判断逻辑。

多站点的菜单注册本质上是个"命名空间扁平化"的设计缺陷,没有自动前缀隔离,全靠开发者自觉。我的建议是网络级菜单 slug 强制加 `network_` 前缀,站点级加 `site_`,虽然丑,但能救命。

你们多站点插件上线前还踩过哪些"权限泄漏"的暗坑?欢迎补刀。

评论0
回复 · 0
还没有回复
微信客服 微信客服