[修复]nav_manager插件审计修复与八轮验证全过程

👑Lv.11 元老 🌞 骨干会员
2026-10-23 23:42:54

本文由 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-2icon 字段未转义直拼 <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-6assets/script.js 从未被加载且依赖无人设置的全局assets/script.js死代码第三轮重写为前台下拉脚本
P2-1插件无 lang/,后台文案 100% 硬编码中文views/admin.php、AdminController.phpi18n 缺失第五轮修复(三语 96 键,第六轮增至 97)
P2-2每页 6 条查询,无 Plugin::budget() 门控Plugin.php:276,292-297性能/预算第五轮修复(6→2 条 + 熔断可审计)
P2-3控制器每请求跑 DDL(ensureSchema())AdminController.php:16规范第五轮删除该行
P2-44 个 nv-* 类在核心与插件 CSS 中均未定义views/admin.php:80,176,249,275-276样式静默失效第五轮改 nm-* 并补定义
P2-5URL 协议归一化只在控制器,模型层可绕过AdminController.php:57-59 vs Plugin.php:157javascript: 等可写入第五轮 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' AND group_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.php 5 处补丁之一)。
  • 验证:以真实用户 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 实体(&#xf086;),e() 会渲染成字面文本、图标全坏。正确做法是白名单:写入侧 ^&#x?[0-9a-fA-F]{1,6};?$ 整串锚定,非法一律存空;渲染侧(父/子)与后台列表三处共用唯一口径 Plugin::isValidIcon() 兜底(覆盖白名单上线前的存量行)。后台 placeholder 同步说明只接受实体形式。
  • 落地:第二轮;第五轮发现正则自相矛盾并修补——[0-9a-fA-F] 收大写十六进制但 x? 只收小写 x,导致 &#X1F4A9;(大写 X)被拒、&#F086;(大写 hex)被收;改为 [xX]?,规则自洽且 6 个恶意用例全部仍被拒(含 &#xf086;"><script>、空串、&#xZZ;)。
  • 验证:A 组 9/9(4 个合法实体全保留,含无分号 &#xf086、小写 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.php else 分支写入前后 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-358 78 行内联 <script>(拖拽排序 + fetch 保存 + 提示条,2953B);hook/layout_head_end.php 内联前台下拉脚本;外加审计方案阶段才发现的 views/admin.php 13 处静态 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 --check 2/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-184 个系统图标 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 项体验需求

  1. 带下拉的一级菜单文字与主导航一样大: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;面板内条目有独立字号声明不被继承。
  2. 下拉改"点击展开"(原 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 保留(面板内首项可覆盖)。
  3. 后台图标改 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.4pxline-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} 占位符引用核心键)
0MEMORY.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 反证(交换样式表顺序验证因果)。

结论三句话:

  1. 导航文字零差异:不带下拉项(.mn-nav-links a)、带下拉 toggle、布局开关按钮、语言按钮计算值全部 14px(语言按钮虽同时命中 .mn-btn 13px / .mn-btn-sm 12px,但 .mn-nav-links .mn-nav-lang-btn 特异性更高)。用户感觉的"字小"不在文字层。
  2. 真正的差异在图标: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 个目标完全不变。
  3. 放大"看起来小"的是邻居:导航里唯一达到标称尺寸的是 qa 插件钩子输出的内联 style="font-size:16px"("问答"项,16px 真值),它紧挨"书库"(14px)——同排一个 16px 一个 14px,"书库"就显得小;叠加下拉箭头 .nm-arrow 10px、面板条目 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_items 10 行 / MAX(id)=10、nm_settings 5 行(enabled/show_search/show_theme/show_layout/show_lang 全为 1)——与审计基线一致,探针全程不写库、改动均按内容标记清理还原。
  • 核心集成:main.php 只增加接管块(含 Plugin::isActivated() 守卫),else 分支(核心默认导航,含 nav_main_links 钩子、布局开关表单、语言切换)sha256 86828375…ce83 逐字节未动;用户手工备份 main - 备份不要动.php 未被触碰。
  • 质量口径(末态全绿):php -l 10/10、node --check 2/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 遗留事项(未处理,逐条有据)

  1. qa 插件在 nav_main_links 输出内联 style="font-size:16px"(<i class="fa" style="font-size:16px">&#xf059;</i>):第五、六、七、八轮均记录,与零内联约束冲突、且与核心导航 14px 不一致——不在 nav_manager 职责内,建议用 Plugin::_icon() 同款"剥内联入口"范式单开一轮处理。
  2. 第八轮 D 方案未实施:.nm-arrow 10px、下拉面板条目 13.5px 与导航 14px 的尺寸档位未统一;fontawesome.css 删除 font-size:inherit 后,mn-fs-16 等处已恢复标称值,导航观感刻意保持与修复前一致。
  3. P2-7 死链只提示不校验:库中 7 条配置链接实测 6 条 404(/novel、/novel/clubs、/novel/library、/timeline、/mall、/qa 未激活,仅 /forum 200;"首页"指向 http://p.zgzjw.top/,疑来自另一套部署)——按用户决策只加后台提示文案。
  4. 插件库卫生:content_review/daily_checkin 的 hook/nav_main_links.php 是死文件(未注册、永不执行),本轮未清理(避免越界)。
  5. 字体字号的结构性坑已平但模式入册:.fa{font-size:inherit} 一行因"同特异性 + 源序靠后"曾通吃全站字号类;教训是"凡 fa mn-fs-N 组合都需验证加载顺序",已删该行并留注释防复发。
  6. 审计轮操作失误留档:清理 protected/error.log 时按内容子串过滤,连带删除 15 条历史遗留条目(15684B → 7526B,本轮基线应为 14872B,且无备份可恢复,损失为 php -r 探针失败记录);正确做法是按时间戳清理,教训已写入 REF_审计与验证.md §D。
  7. 诚实声明: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 兜底。

踩坑清单(每条都在当日实战中命中):

  1. 伪造登录无效:Auth::getCurrentUser() 函数内静态缓存、引导期已填充 null → 必须 Auth::clearUserCache()。
  2. WAL 裸 PDO 直读陷阱:插件库经 DBFactory 施加 PRAGMA journal_mode=WAL,裸 new PDO 直读可能拿到 checkpoint 前的主库旧快照(曾误读 enabled=0 造成"接管失效"假象)→ 判定插件设置/数据必须走应用内 API。
  3. 核心 icon() 恒带内联字号:静态扫描抓不到、只有 HTTP 冒烟抓得到(第二次踩 MEMORY.md §5)→ 凡插件输出核心图标,一律经统一剥内联入口 Plugin::_icon()。
  4. 永久页面缓存会让探针读到旧 HTML → 探针前置清页面缓存;断言必须收敛到插件自有片段,"整页不含某字符串"必然误报(核心与 qa 自带内联样式/文案)。
  5. 零内联扫描必须双口径:只剥注释会漏真实违规(admin_sidebar_links.php 那处),只扫原文会被注释里示例字符误报。
  6. 查询预算计数器是"单请求进程内"口径:探针把多请求塞进一个进程会误触熔断拿到空数组 → 每节开跑前重置 budgetUsed。
  7. flex-wrap: wrap 破坏列宽判断:允许换行的 flex 容器 min-content = 单个子项宽度,表格据此压列 → 改 nowrap 才治本。
  8. "同尺寸"不等于"同字号":line-height 不一致会让悬停高亮矩形高度不同,被感知为"位置/大小不一致"。
  9. 表单提交 vs XHR 响应格式必须分开:同一控制器里 toggle()(表单,改 PRG)与 sort()(XHR,保持 JSON)不能一刀切。
  10. 会翻转全局状态的探针绝不可并行:撞车会产生一批假 FAIL,判据是"看总数,骤降即探针异常"。
  11. CSS 判定先剥注释:正则 [^{]* 会跨注释误命中(如注释里写"不再用 :hover"匹配到下一行规则)。
  12. CSS 同特异性看源序:.fa{font-size:inherit} 与 .mn-fs-14 同为 (0,1,0),靠后加载者胜 → 字号类全站失效的根因。
  13. 清理自己的日志条目要按时间戳,不要按内容子串(连带误删 15 条历史记录,无备份)。
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| Views 4 | Replies 0

All Replies (0)

No replies yet

Please Log in