本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:合并篇(见正文内 merged_from 列表)。
nav_manager 插件审计修复与八轮验证全过程
本篇由 9 篇原始文档合并去重而成(1 篇审计 + 1 篇修复方案 + 6 轮验证 + 1 份插件说明)。
轮次口径:第一轮只交方案、不动代码;实际改动自第二轮起。原计划中的"第四轮"没有独立报告,第五批(P2 全部)的落地与验证由第五轮报告承载,故全套实证轮次为第二、三、五、六、七、八轮。
一、插件定位与背景
nav_manager(导航管理 v1.0.0,author=读到书院)的作用是接管站点顶部导航菜单:自定义菜单项、图标、二级下拉、可见性控制(仅登录用户可见 / 仅管理员可见)。
- 后台入口:后台 → 导航管理(
/admin/nav-manager) - 数据:插件独立库
plugins/nav_manager/data/nav_manager.sqlite(表nm_items/nm_settings) - 前台渲染:核心
app/Views/layouts/main.php调用Plugin\NavManager\Plugin::renderTopNav() - 多语言:
plugins/nav_manager/lang/{zh,zh_tw,en}.php,由I18n::mergePluginLangs()自动合并,插件无需写钩子
治理前的状态:插件本体 8 个 PHP/JS/CSS 文件 + 数据目录已就位,但 main.php 未打补丁,前台完全惰性(菜单项不出现,实测确证)。2026-09-24 单日完成"审计 → 方案 → 六轮滚动验证"的全过程。
全程处置边界(所有轮次共同遵守):
- 不碰
app/Core/、app/SplitDB/、app/Models/、app/Helpers/ main.php只包一层 if/else,else 分支(核心默认导航)一字未改,落地前后 sha256 逐字节一致- 不改
plugin.json钩子清单、hook/init_after.php、hook/admin_route_register.php、hook/admin_sidebar_links.php - 不动其他插件;探针一律放
.workbuddy-ai/tmp/,数据按内容标记清理、用完还原
二、审计发现的问题(按严重度)
首轮审计(静态代码审计 + php -S 运行时实测 + 伪造会话 + 反射调用 private 方法 + 数据实测)共立项 2 项 P0 + 6 项 P1 + 8 项 P2, verdict 如下("最终状态"为当日收官时的状态):
| 编号 | 问题 | 位置/文件 | 风险 | 最终状态 |
|---|---|---|---|---|
| P0-1 | 自造 isAdmin:把 group_id===1(=普通会员)判为管理员 | Plugin.php:278-290 | 所有登录用户可看 require_admin=1 菜单项;与既有 site_stats 审计 P0-1 同根因,构成复发性缺陷 | 第二轮修复并复测通过 |
| P0-2 | icon 字段未转义直拼 <i class="fa">,前台全站渲染 | Plugin.php:343,364;views/admin.php:225 | 存储型 XSS 汇聚点(管理员自伤升级为全站) | 第二轮修复(白名单),第五轮补正则自洽 |
| P1-1 | 接管后核心 nav_main_links 钩子不再触发,插件自身无任何 hook() 调用 | Plugin.php:273-312 | 钩子契约破坏、静默失效 | 第二轮修复;影响面经实测修正 |
| P1-2 | 下拉按钮硬编码 color:#fff,默认主题白底白字不可见 | assets/style.css:123 | 主题安全,肉眼可见 | 第三轮修复 |
| P1-3 | --mn-bg-soft 在 10 个主题文件中全部未定义 | assets/style.css:49 | 深色主题浅底浅字 | 第三轮修复 |
| P1-4 | 游客页走永久页面缓存,接管后菜单改动对游客不生效 | data/runtime/pages/ | 运维硬依赖 | 第三轮文档化;第七轮改为启用/禁用自动清缓存 |
| P1-5 | 零内联违规:onsubmit= + 两段内联 <script>(另有 13 处静态 style="") | views/admin.php:259,281-358;hook/layout_head_end.php | 规范违规 | 第三轮修复 |
| P1-6 | assets/script.js 从未被加载且依赖无人设置的全局 | assets/script.js | 死代码 | 第三轮重写为前台下拉脚本 |
| P2-1 | 插件无 lang/,后台文案 100% 硬编码中文 | views/admin.php、AdminController.php | i18n 缺失 | 第五轮修复(三语 96 键,第六轮增至 97) |
| P2-2 | 每页 6 条查询,无 Plugin::budget() 门控 | Plugin.php:276,292-297 | 性能/预算 | 第五轮修复(6→2 条 + 熔断可审计) |
| P2-3 | 控制器每请求跑 DDL(ensureSchema()) | AdminController.php:16 | 规范 | 第五轮删除该行 |
| P2-4 | 4 个 nv-* 类在核心与插件 CSS 中均未定义 | views/admin.php:80,176,249,275-276 | 样式静默失效 | 第五轮改 nm-* 并补定义 |
| P2-5 | URL 协议归一化只在控制器,模型层可绕过 | AdminController.php:57-59 vs Plugin.php:157 | javascript: 等可写入 | 第五轮 normalizeUrl() 下沉模型层 |
| P2-6 | 导航图标尺寸与核心不一致(核心恒带字号) | Plugin.php:343,364 | 视觉不一致 | 第五轮挂 mn-fs-14 + _icon() 剥内联 |
| P2-7 | 种子/现有配置指向不存在的路由,插件不校验 | Plugin.php:73-92 | 前台渲染死链 | 滞后方按用户决策 B:只加后台提示,不做校验 |
| P2-8 | 拖拽排序不改 parent_id,UI 未说明 | Plugin.php:219-227 | 语义误导 | 第五轮补三语文案 + 事务化 |
合规通过项(20 项,首轮即确认无问题):class_alias、生命周期三方法、init_after 版本门控带数据层版本号、Admin\BaseController 门禁、CSRF 5 处全对、写路由全 POST、SQL 全参数绑定、key/value 反引号转义、data/ 双重目录防护、permissions 声明、后台路由前缀、40 个 nav.* 键三语零缺、listTree() 引用别名无污染、无 BOM、LF、.schema_ver 与 $currentVer 一致、sqlite 表结构规范、错误全部 try/catch 不白屏、renderTopNav 返回 null 兜底语义正确、getVisibleTree 递归过滤正确。
首轮诚实声明(未做的验证):未做写操作端到端实测(P0-2 是反射 PoC 而非真实提交注入;P2-5 的 javascript: 中和为静态推理);未在 Xenforo 主题下验证(该主题 layouts/main.php:199 也有 nav_main_links,但没有 nav_manager 集成点 → 接管方案只覆盖默认主题)。
三、修复过程(按问题归并,不按轮次)
以下按问题归并。每条给出:问题 → 改法(涉及文件/函数/配置键)→ 落地轮次 → 验证结果。
3.1 P0-1 越权:普通会员被判定为管理员
- 问题:
Plugin.php:278-290自造管理员判定——条件①!empty($u['is_admin'])(users表无此列,恒 false 死代码);条件②role==='admin'单条件;条件③group_id===1(越权根源)。实测user_groups:1=普通会员(默认组)、2=VIP、3=版主、4=管理员、5=封禁——条件③等价于"所有登录用户都是管理员";且三个条件用||拼接,而权威口径Auth::isAdmin()(app/Helpers/Auth.php:20)是role==='admin'ANDgroup_id===4。探测实测:uid=2(role=user, group_id=1)在插件眼里是管理员、在核心眼里不是。这是本项目第 2 次同一根因(site_stats 审计 P0-1 完全相同)。 - 改法:删掉 3 个自造条件 +
$_SESSION回退,直接调\app\Helpers\Auth::isAdmin()。$isAdmin仅用于getVisibleTree()过滤require_admin=1菜单项(当前库 10 行全为 0,未实际暴露)。 - 落地:第二轮(
Plugin.php5 处补丁之一)。 - 验证:以真实用户 uid=2 作登录态,插入
require_admin=1临时菜单项做正反例对比——4/4 PASS,含对称反证(内存还原旧实现后同一用户确实看到该菜单项,证明修复是载荷性的而非测试没覆盖);探针按内容标记清理,nm_items复原 10 行。 - 踩坑:
Auth::getCurrentUser()有函数内静态缓存、app/init.php引导期即填充为 null → 伪造登录必须补Auth::clearUserCache()(app/Helpers/Auth.php:107,置$cacheDirty=true)。
3.2 P0-2 存储型 XSS:icon 字段未转义
- 问题:写入侧
saveItem()(Plugin.php:158)只 trim 不校验;渲染侧(Plugin.php:343父项、:364子项)与后台列表(views/admin.php:225)原样输出。反射 PoC:<img src=x onerror=alert(1)>、<script>、闭合标签 3 组注入载荷全部原样输出。写入需管理员权限 + CSRF,但一旦写入即对所有访客每次渲染生效。 - 改法:不简单套
e()——$icon正常取值是 HTML 实体(),e()会渲染成字面文本、图标全坏。正确做法是白名单:写入侧^&#x?[0-9a-fA-F]{1,6};?$整串锚定,非法一律存空;渲染侧(父/子)与后台列表三处共用唯一口径Plugin::isValidIcon()兜底(覆盖白名单上线前的存量行)。后台 placeholder 同步说明只接受实体形式。 - 落地:第二轮;第五轮发现正则自相矛盾并修补——
[0-9a-fA-F]收大写十六进制但x?只收小写 x,导致💩(大写 X)被拒、&#F086;(大写 hex)被收;改为[xX]?,规则自洽且 6 个恶意用例全部仍被拒(含"><script>、空串、&#xZZ;)。 - 验证:A 组 9/9(4 个合法实体全保留,含无分号
、小写 hex;5 个注入载荷全清空,含"实体前缀+追加脚本"绕过);B 组 DB 直插恶意 icon 渲染侧兜底 PASS(输出无<img/onerror/script,且菜单项本身仍正常渲染、不因图标非法丢项)。
3.3 P1-1 钩子契约 + main.php 接管集成
- 问题:
main.php接管分支一旦生效,核心Plugin::hook('nav_main_links')(main.php:160,唯一触发点)不再执行,而插件内部无任何Plugin::hook()调用。 - 影响面实测修正(第二轮):审计时"接管后 4 个插件导航入口消失"的结论不成立。三查(有钩子文件 / plugin.json 注册 / 激活 / 钩子有内容)后:
content_review、daily_checkin有文件但未注册(死文件);dice已注册但钩子已清空(迁nav_plugin_links);qa已注册有内容但未激活 →nav_main_links当前零实际产出。修正表述:属契约破坏(现在或将来注册该钩子的插件都会静默失效、无报错),修复仍正确必要。 - 改法:
renderTopNav()在自定义菜单项之后、系统按钮之前补ob_start(); \app\Helpers\Plugin::hook('nav_main_links'); $html .= (string)ob_get_clean();;main.php:154-192只包一层 if/else——class_exists && method_exists判定 + try/catch +error_log+ null 降级,else 分支从原文件读出行列表原样复用(不手抄)。 - 落地:第二轮。
- 验证:端到端 A/B——反射注入临时监听者,正例输出 1730B / 反例 1673B,长度差 57 = 监听者输出串精确长度;
main.phpelse 分支写入前后 sha256 均为86828375…ce83(4107 字节)逐字节一致;HTTP 冒烟 4 路径:管理员/游客/、/thread/58接管行为全部符合预期(游客/仍核心导航 = 前置缓存,即 P1-4 复现)。附带确认$GLOBALS['__view']引导期已赋值,接管路径上其他插件钩子可用。
3.4 P1-2 / P1-3 主题安全(写死色与不存在的变量)
- 问题:P1-2——
style.css:117-131下拉 toggle 硬编码color:#fff,作者假设导航栏恒深色;实测默认主题 modern.css 导航栏--mn-text-nav=#5b6778/--mn-bg-nav=#ffffff(浅色)→ 白底白字。库中"书库"有 4 个子项即渲染为下拉,默认主题下标题不可见。P1-3——--mn-bg-soft在 10 套主题中 0/10 存在,恒回退近白#f8fafc,深色主题下浅底浅字。 - 改法:对齐核心
modern.css:236-266写法——#fff→var(--mn-text-nav, #5b6778);:hover/.nm-open与.mn-active补var(--mn-text-nav-hover)+var(--mn-bg-nav-hover)(顺手去掉自造的color-mix(currentColor…)半透明底);--mn-bg-soft→var(--mn-bg-row-hover)(10/10 存在)。 - 落地:第三轮。
- 验证:插件 CSS 用到的 12 个
--mn-*变量 × 10 套主题 = 120/120 全覆盖(脚本化,非抽样);--mn-bg-soft全插件残留 0;style.css 剩余 5 处#fff逐处复核合理(var()兜底 2 处、固定橙红渐变上白字 1 处、注释 2 处)。
3.5 P1-4 缓存硬依赖(从"写文档"到"自动化")
- 问题:
index.php:22-26的前置静态缓存对纯游客 GET 列表页(首页/论坛/版块)直接输出data/runtime/pages/*.html,跳过整个框架引导(插件不执行),且永久有效、不按 TTL 过期,只由发帖/回帖PageCache::invalidate()主动失效。菜单改动与插件 CSS/JS 都进不去已缓存页;带未登录 Cookie 的请求也命中,只有登录态走完整管线。 - 改法(第三轮·文档化):写入
plugins/nav_manager/说明.md;后台views/admin.php的.nm-hint提示块加"改完菜单必须清缓存",按钮名引用核心键admin.mt_clear_cache占位符(不写死"维护工具/更新缓存",核心改名这里跟着变)。 - 改法(第七轮·自动化):
activate()/deactivate()/uninstall()三处挂新增私有方法invalidatePageCache()(PageCache::invalidate()+glob逐个@unlink兜底——Windows 文件占用会静默失败;整段 try/catch——绝不能因它抛异常让activate()返回 false,否则核心会回滚激活态;uninstall()也要清,因为核心卸载流程不走deactivate())。 - 验证:
nm_r7_autocache.py五步全 PASS——生成接管态缓存 → 禁用插件(不人工清)→ 缓存文件 = 0 → 游客 GET 立刻核心导航 → 重新启用 → 缓存文件 = 0 → 立刻回到接管态且 CSS/JS 齐备。
3.6 P1-5 零内联 + P1-6 死代码复活
- 问题:P1-5——
views/admin.php:259的onsubmit="return confirm(…)";views/admin.php:281-35878 行内联<script>(拖拽排序 + fetch 保存 + 提示条,2953B);hook/layout_head_end.php内联前台下拉脚本;外加审计方案阶段才发现的views/admin.php13 处静态style=""(grid-column:1/-1、display:flex…、font-size:16px等)。P1-6——assets/script.js(4.4KB)是客户端重建导航的旧实现(读window.__NM_CONFIG__清空.mn-nav-links重绘),从未被 enqueue、依赖的全局无人设置。 - 改法:13 处静态 style → 工具类(
nm-col-span/nm-check-row/nm-check-item/nm-inline-form/nm-mt-10等,定义在style.css末尾,JS 侧拖拽态/提示条用classList挂nm-dragging/nm-saved-hint);onsubmit→data-nm-confirm+assets/admin.js(新增)事件委托;78 行内联 script →admin.js外链(?v=filemtime+ defer,main.php 无yield('js'),零核心改动);layout_head_end.php内联脚本 → 重写assets/script.js(4421B → 1585B,删除__NM_CONFIG__旧实现,改为移动端/PC 统一点击展开),hook 只留两行外链并加/admin前缀早退。 - 额外发现(审计报告漏列):
hook/admin_sidebar_links.php有一处<i class="fa" style="font-size:14px">→ 改mn-fs-14。教训:零内联扫描必须双口径(原文扫描 + 剥注释扫描),单口径会漏。 - 验证:静态零内联 0/0/0(10 文件);
node --check2/2;三个静态资源 HTTP 200;前后台各 2 处 FAIL 均为探针口径过宽(核心 JSON-LD/CSRF/i18n/PWA 自有<script>、核心语言按钮内联,nav_manager标识命中 0)而归因排除。
3.7 第七轮实战暴露的新问题:禁用插件后前台导航错乱(原审计清单外)
- 现象(用户报障"后台禁用插件前台导航会乱"):禁用后游客首页接管 HTML 还在、插件 CSS/JS 没了——下拉面板靠插件的
.nm-nav-dropdown-panel{display:none}才隐藏,CSS 一缺席,子项整片展开铺进导航条。 - 根因三层叠加,缺一不可:①
app/Core/Autoloader.php:36对Plugin\命名空间无条件注册(不按激活态门控)→ 禁用后class_exists仍为 true;②main.php:159只判class_exists && method_exists,照样调renderTopNav()(nm_settings.enabled仍是 '1',禁用插件不改业务开关);③app/Helpers/Plugin.php:609的Plugin::hook()跳过未激活插件 →layout_head_end不再分发 → CSS/JS 不注入。HTML 走直接函数调用(不受激活态约束),CSS/JS 走钩子(受约束),两者脱钩。 - 改法:① 核心调用点补
Plugin::isActivated('nav_manager')守卫(main.php,实为补齐原注释早就写下的意图,else 分支未动);②renderTopNav()自己也兜底return null(不依赖单一调用点的自觉,防将来到其他调用方);③invalidatePageCache()挂三处生命周期(见 3.5)——否则禁用后缓存页仍是接管外观,管理员误判"禁用没生效"。 - 验证:A/B 同探针同断言——复现 15/7 → 修复后 22/0;enabled 态回归 16/0;全量回归(php -l、静态零内联、i18n 97 键、第六轮 HTTP 77/0)全绿。
- 踩坑:把翻转插件激活态的 r7 与全量回归 r6 并行跑 → r6 撞上"禁用"瞬间报 8 个假 FAIL(总数 77→30)。判据:看总数,总数骤降 = 探针中途异常,不是产品回归;会翻转全局状态的探针必须串行。
3.8 第五批规范项(P2-1 ~ P2-8,第五轮一次落地)
| 项 | 改法 | 验证结果 |
|---|---|---|
| P2-1 i18n | 新建 lang/{zh,zh_tw,en}.php,键前缀 plugin.nav_manager.*(第五轮各 96 键,第六轮 97);视图用 $this->t(),控制器用 I18n::get() | 三语键集合零差异 + 键序一致;引用完整性(77 个静态引用全命中);未引用键 0;HTTP 后台三语渲染 PASS |
| P2-2 查询 | 新增 allSettings()(一条 SELECT key,value + FETCH_KEY_PAIR + 进程内缓存),getSetting() 改读它;补 Plugin::budget() / QUERY_BUDGET=5 / budgetUsed()(范式见 user_level/Plugin.php:47) | 每页 6 → 2 条(二次渲染 0 条);熔断断言:额度占满后 budget(1)===false、listAll()===[] 降级不查库;18 条"查询预算超限 (6>5)"日志为探针故意触发,修后带管理员会话实测新增 0 条 |
| P2-3 DDL | 删 AdminController.php:16 每请求 ensureSchema(),保留 activate() + init_after 版本门控双保险 | .schema_ver=2 命中,请求路径不再建表 |
| P2-4 样式类 | nv-form-actions/nv-row-actions/nv-admin-footer/nv-admin-back → nm-* 并在 style.css 补定义 | 视图 32 个 nm- 类全部有定义(CSS 共定义 41 个);nv- 残留 0 |
| P2-5 URL | 校验下沉模型层:新增 Plugin::normalizeUrl() 唯一口径,拒绝 javascript:/data:/vbscript:(大小写不敏感、含 java\nscript: 控制字符绕过)与 //evil.com 协议相对地址,其余按 /、http、mailto: 归一化;控制器重复逻辑删除 | 11 个用例全 PASS;非法输入落库前清空 + flash 提示 |
| P2-6 图标字号 | 导航父/子图标 <i class="fa"> → <i class="fa mn-fs-14">;核心 icon() 恒带内联 style="font-size:Npx"(静态扫不到、HTTP 冒烟才抓到,第二次踩 MEMORY.md §5 的坑)→ 新增统一入口 Plugin::_icon($view,$name,$size),preg_replace_callback 把核心返回值改写成 mn-fs-N(正则异常退回原串绝不丢图标);后台 <h1> 改字面实体 + mn-fs-18 | 4 个系统图标 4/4 sized=True inline=False;后台内容区内联字号命中 0 |
| P2-7 路由校验 | 用户拍板 选 B:不做 Reflection(Router::$routes 是 protected),只加 plugin.nav_manager.tip_url 三语文案(相对路径需对应真实路由) | 视图渲染确认;校验本身未做(名存实亡是已知取舍) |
| P2-8 拖拽语义 | 补 plugin.nav_manager.tip_drag_semantics 三语文案:"拖拽只调整同级顺序,不会改变层级(层级请用父级菜单设置)";sortItems() 循环 UPDATE 包 beginTransaction/commit/rollBack,写后 unset(self::$cache['items'])(saveItem/deleteItem/toggleEnabled 同样失效缓存) | 反转 → 落库值确实改变;listAll 顺序 == 纯 PHP 复刻的 SQL 排序模型;还原 == 原顺序,快照精确还原 |
3.9 第五轮用户追加的 3 项体验需求
- 带下拉的一级菜单文字与主导航一样大:
style.css把.nm-nav-dropdown-toggle排版逐项对齐核心modern.css:236-247——padding:6px 14px、border-radius:6px、font-size:14px、font-weight:450、inline-flex、gap:5px;面板内条目有独立字号声明不被继承。 - 下拉改"点击展开"(原 hover):删
@media (min-width:769px)下的:hover展开块,只保留.nm-nav-dropdown.nm-open > .nm-nav-dropdown-panel{display:block};箭头旋转判定改挂.nm-open;script.js删 PC 早退innerWidth(PC/移动端统一点击)、preventDefault()阻止 toggle 跳转、Esc 关闭、点面板内不关。toggle 的href保留(面板内首项可覆盖)。 - 后台图标改 20 个内置图标选择器:
Plugin::iconPalette()新增 20 条,码点全部取自核心TemplateCompiler::icon()映射表(保证 woff2 有字形);自由文本输入 → 隐藏input[name=icon]+nm-icon-picker(20 个data-nm-cp按钮 + 1 个"无图标");admin.js委托写'&#x'+cp+';';三语各 20 个图标名。库里存的仍是 HTML 数字实体,历史数据不受影响。
3.10 第六轮用户反馈的 4 项 + MEMORY 上限调整
| # | 用户描述 | 真实根因 | 处置 |
|---|---|---|---|
| 1 | 带下拉的一级菜单"比不带下拉的稍微低一些" | 字号一直是同一个 14px。真凶是 line-height:核心 .mn-nav-links a 不声明(继承 body{line-height:1.6}),插件写死 1.4 → 盒高 31.6px vs 34.4px,悬停高亮矩形上沿低 1.4px | line-height: 1.4 → 1.6("同尺寸"要连"没写"一起照抄) |
| 2 | 后台点"禁用/启用"出现裸 {"ok":true} | AdminController::toggle() 末尾 $this->json(['ok'=>true]),但它是普通表单提交(非 XHR),浏览器把 JSON 当页面渲染 | 改 PRG:写 $_SESSION['nm_flash_ok'] + redirect('/admin/nav-manager');新增 i18n 键 plugin.nav_manager.msg_item_toggled(三语);sort() 的 JSON 必须保留(admin.js 拖拽保存靠它读响应),加对照断言防误改 |
| 3 | 操作列 3 个按钮被挤成 2 行 | 列宽 180px 不够只是表层;.nm-row-actions{flex-wrap:wrap} 让容器 min-content = 单个按钮宽,表格自动布局据此压列 | 列宽再分配:排序 80→60、可见性 90→80、操作 180→220(新增 .nm-w-220、删无引用的 .nm-w-180);flex-wrap 改 nowrap 治本 |
| 4 | 顶部提示"大提示 + 小提醒"各占一行 | 原实现只有 1 个 <br>,4 段文案靠容器宽度自动折行,宽屏拼成一行 | 3 个 <br> 强制 4 行;文案按用户给定原文对齐(hint_takeover_2/hint_cache_2 三语同步,保留 {menu}/{btn} 占位符引用核心键) |
| 0 | MEMORY.md 标注的 8000 上限改 12000 | 顶部自述 8000,实际 10378 字符已超约 30% | 自述值校正为 12000(改后 10379,余量 1621) |
3.11 第八轮:导航字号审计(只读诊断、零改动)
用户提问"老是感觉带下拉的导航菜单字号小一些,是错觉吗"。本轮未修改任何源码/样式,方法:CSS 层叠求解器(按 main.php 真实样式表注入顺序 plugin style.css → modern.css → modern-night.css → fontawesome.css → editor.css,解析 943 条规则,逐目标求胜出声明)+ A/B 反证(交换样式表顺序验证因果)。
结论三句话:
- 导航文字零差异:不带下拉项(
.mn-nav-links a)、带下拉 toggle、布局开关按钮、语言按钮计算值全部 14px(语言按钮虽同时命中.mn-btn13px /.mn-btn-sm12px,但.mn-nav-links .mn-nav-lang-btn特异性更高)。用户感觉的"字小"不在文字层。 - 真正的差异在图标:
fontawesome.css:15的.fa { font-size: inherit }与modern.css:1852的.mn-fs-14特异性完全相同 (0,1,0),按源序靠后者胜——fontawesome.css(main.php:63注入)晚于 modern.css(main.php:51)→ inherit 通吃所有.fa.mn-fs-N字号类,导航里每个图标都按父级 14px 渲染,mn-fs-16/mn-fs-18全是死类(.fa还有width:1.25em,外框宽度同步缩水)。A/B 反证:把 fontawesome.css 提前后mn-fs-14立即反超,其余 6 个目标完全不变。 - 放大"看起来小"的是邻居:导航里唯一达到标称尺寸的是
qa插件钩子输出的内联style="font-size:16px"("问答"项,16px 真值),它紧挨"书库"(14px)——同排一个 16px 一个 14px,"书库"就显得小;叠加下拉箭头.nm-arrow10px、面板条目 13.5px 两档更小尺寸,整体被读成"这个菜单小一些"。
全站影响面:活站点共 15 处 class="fa mn-fs-N",其中 5 处真错(icon_generator/props 的 mn-fs-16、mod_system 的 mn-fs-15、nav_manager 后台 views/admin.php:157/259 的 mn-fs-16——父级 13px 差 3px 最明显);其余属"恰好对"(mn-fs-14 在导航里恰好等于父级 14px,纯属巧合)。
排除的嫌疑因素:9 套主题只设导航 color/background/border,无一处设 font-size;line-height 已在第六轮修过;?v= 缓存字节一致已生效;插件禁用样式缺失已由第七轮修掉;但 modern.css:9 的 html{font-size:14px}(非 16px)是真实因素——任何 em 单位以 14px 为基准。
审计报告给出的候选与后续实施:A 调换样式表顺序(动 main.php 修全站但需全站回归)、B 提高字号类特异性(加 .fa.mn-fs-N,精准零风险)、C 逐处改写(治标)、D 顺带统一导航尺寸档位(改观感需用户确认);建议 B+D 但报告本身零改动。后续用户拍板"直接删 fontawesome.css:15 那一行就够了"并实施:删除 .fa { } 块内 font-size: inherit;,原地留 4 行说明注释防将来被加回(5312B → 5775B,.fa-lg/.fa-2x/.fa-3x/width/line-height 原样保留)。A/B 反证按计算值口径:导航整排(T1~T5/toggle 图标/箭头)零变化无回归,X1(导航 mn-fs-16)/X2(后台表格)/X4(抽屉 mn-fs-15)三处真错修复;HTTP 冒烟 22/0,asset() 自动破缓存(?v=1790249103 = 新 mtime)生效。D 方案未实施:.nm-arrow 仍 10px、面板条目仍 13.5px、qa 内联 16px 未动——前台导航外观与修复前完全一致(只修失效类、不改观感)。
四、最终状态与遗留
4.1 最终状态(收官验收口径)
- 插件状态:
isActivated=true、plugin.json.activated=true;nm_items10 行 /MAX(id)=10、nm_settings5 行(enabled/show_search/show_theme/show_layout/show_lang全为 1)——与审计基线一致,探针全程不写库、改动均按内容标记清理还原。 - 核心集成:
main.php只增加接管块(含Plugin::isActivated()守卫),else 分支(核心默认导航,含nav_main_links钩子、布局开关表单、语言切换)sha25686828375…ce83逐字节未动;用户手工备份main - 备份不要动.php未被触碰。 - 质量口径(末态全绿):
php -l10/10、node --check2/2;零内联双口径 0/0/0;--mn-变量 12 个 × 10 套主题 120/120;视图nm-类 32 个全部有定义;i18n 三语 97 键,键集合零差异 + 键序一致、未引用键 0;前台每页查询 2 条(预算上限 5,可审计、超限降级并写日志);全部文件 UTF-8 无 BOM + 纯 LF。 - 验证矩阵(末态):第六轮 HTTP 77/0;第七轮 disabled 22/0 + enabled 16/0 + 缓存失效 20/0;第八轮实施后 HTTP 22/0。
- 探针纪律(每轮收尾共同项):
nm_items/nm_settings还原基线;冒烟会话按固定 SID 删除、真实登录会话保留 1 行;页面缓存清 0;站点根无残留探针(全在.workbuddy-ai/tmp/);error.log无本轮新增(除预算熔断断言故意触发的历史记录)。
4.2 遗留事项(未处理,逐条有据)
qa插件在nav_main_links输出内联style="font-size:16px"(<i class="fa" style="font-size:16px"></i>):第五、六、七、八轮均记录,与零内联约束冲突、且与核心导航 14px 不一致——不在 nav_manager 职责内,建议用Plugin::_icon()同款"剥内联入口"范式单开一轮处理。- 第八轮 D 方案未实施:
.nm-arrow10px、下拉面板条目 13.5px 与导航 14px 的尺寸档位未统一;fontawesome.css删除font-size:inherit后,mn-fs-16等处已恢复标称值,导航观感刻意保持与修复前一致。 - P2-7 死链只提示不校验:库中 7 条配置链接实测 6 条 404(
/novel、/novel/clubs、/novel/library、/timeline、/mall、/qa未激活,仅/forum200;"首页"指向http://p.zgzjw.top/,疑来自另一套部署)——按用户决策只加后台提示文案。 - 插件库卫生:
content_review/daily_checkin的hook/nav_main_links.php是死文件(未注册、永不执行),本轮未清理(避免越界)。 - 字体字号的结构性坑已平但模式入册:
.fa{font-size:inherit}一行因"同特异性 + 源序靠后"曾通吃全站字号类;教训是"凡fa mn-fs-N组合都需验证加载顺序",已删该行并留注释防复发。 - 审计轮操作失误留档:清理
protected/error.log时按内容子串过滤,连带删除 15 条历史遗留条目(15684B → 7526B,本轮基线应为 14872B,且无备份可恢复,损失为php -r探针失败记录);正确做法是按时间戳清理,教训已写入REF_审计与验证.md§D。 - 诚实声明:P0-2 的 XSS 为反射 PoC 而非端到端真实注入;Xenforo 主题(
layouts/main.php:199有nav_main_links但无 nav_manager 集成点)未验证——接管只覆盖默认主题,切主题后导航不接管。
附录 A:插件功能速览(据 plugins/nav_manager/说明.md 整理)
- 接管语义:开启后核心默认项(首页/论坛/博客/搜索/主题/语言/布局)不再显示,顶部菜单完全由插件列表决定,系统按钮在"接管开关"里勾选;接管关闭、插件被禁用或渲染异常时自动回退核心默认导航,不白屏。
- 钩子兼容:自定义菜单项之后继续触发核心
nav_main_links,其他插件经该钩子挂的导航入口不因接管而消失。 - 下拉交互:PC 与移动端统一点击展开/收起;点面板外、按 Esc、点开另一个下拉时自动收起;带下拉的导航项本身不跳转(要跳转就把目标地址作为子项添加);字号/字重/内边距/行高与普通导航项完全一致(都对齐核心
.mn-nav-links a,行高靠继承body{line-height:1.6})。 - 图标字段:后台为系统内置 20 个常用图标选择器(码点取自核心
TemplateCompiler::icon()映射表,保证 woff2 有字形);库存 HTML 数字实体,非法值写入时清空(白名单唯一实现Plugin::isValidIcon(),写入/前台渲染/后台列表三处共用);留空 = 不显示图标。 - URL 规则:
/forum原样;forum自动补/;https://、mailto:原样;javascript:/data:/vbscript:///evil.com拒绝;归一化唯一口径Plugin::normalizeUrl()(模型层);站内路径需对应真实路由(插件不校验,只提示)。 - 拖拽排序:拖
⋮⋮手柄调序、松手自动保存;只改同组内相对顺序、不改变层级(层级由"父级菜单"下拉框决定)。 - 性能约定:每次
renderTopNav()只查 2 条(allSettings()1 +listAll()1),带进程内缓存、同请求重复调用不查库;所有读查询过Plugin::budget()闸门,单插件每请求上限 5 条,超限降级并error_log。 - 运维硬依赖:每次改动菜单/接管开关后,必须到「后台 → 维护工具 → 更新缓存」执行一次(游客列表页走永久页面缓存);登录用户不受影响;插件的启用/禁用/卸载已自动清缓存(第七轮起)。
附录 B:验证方法与踩坑沉淀(跨轮复用)
方法基线:php -S 独占端口冒烟 + 手写 sessions 行(测毕 DELETE);关键改动后回读 + sha256(本机 Edit 偶发假成功);补丁用 Python 原始字符串锚点施加、锚点唯一性 count==1 断言;探针全部放 .workbuddy-ai/tmp/,不指定 id、按内容标记清理并挂 register_shutdown_function 兜底。
踩坑清单(每条都在当日实战中命中):
- 伪造登录无效:
Auth::getCurrentUser()函数内静态缓存、引导期已填充 null → 必须Auth::clearUserCache()。 - WAL 裸 PDO 直读陷阱:插件库经
DBFactory施加PRAGMA journal_mode=WAL,裸new PDO直读可能拿到 checkpoint 前的主库旧快照(曾误读enabled=0造成"接管失效"假象)→ 判定插件设置/数据必须走应用内 API。 - 核心
icon()恒带内联字号:静态扫描抓不到、只有 HTTP 冒烟抓得到(第二次踩MEMORY.md §5)→ 凡插件输出核心图标,一律经统一剥内联入口Plugin::_icon()。 - 永久页面缓存会让探针读到旧 HTML → 探针前置清页面缓存;断言必须收敛到插件自有片段,"整页不含某字符串"必然误报(核心与
qa自带内联样式/文案)。 - 零内联扫描必须双口径:只剥注释会漏真实违规(
admin_sidebar_links.php那处),只扫原文会被注释里示例字符误报。 - 查询预算计数器是"单请求进程内"口径:探针把多请求塞进一个进程会误触熔断拿到空数组 → 每节开跑前重置
budgetUsed。 flex-wrap: wrap破坏列宽判断:允许换行的 flex 容器 min-content = 单个子项宽度,表格据此压列 → 改nowrap才治本。- "同尺寸"不等于"同字号":line-height 不一致会让悬停高亮矩形高度不同,被感知为"位置/大小不一致"。
- 表单提交 vs XHR 响应格式必须分开:同一控制器里
toggle()(表单,改 PRG)与sort()(XHR,保持 JSON)不能一刀切。 - 会翻转全局状态的探针绝不可并行:撞车会产生一批假 FAIL,判据是"看总数,骤降即探针异常"。
- CSS 判定先剥注释:正则
[^{]*会跨注释误命中(如注释里写"不再用:hover"匹配到下一行规则)。 - CSS 同特异性看源序:
.fa{font-size:inherit}与.mn-fs-14同为 (0,1,0),靠后加载者胜 → 字号类全站失效的根因。 - 清理自己的日志条目要按时间戳,不要按内容子串(连带误删 15 条历史记录,无备份)。