本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/docs/插件审计报告.md。
FlintHub 插件审计报告
| 项目 | 内容 |
|---|---|
| 审计对象 | plugins/ 下全部插件(28 个目录) |
| 审计依据 | docs/插件开发规范.md v1.3(2026-09-20) —— §一 三条硬性规则、§一五 八条最高级别红线、§二~§十一 结构与红线清单 |
| 审计方式 | 静态代码审计(词法预处理 + 规则匹配)+ 关键点源码逐条人工复核 + PHP 语法校验 + 钩子双 include 回归 |
| 审计时间 | 2026-09-21 |
| 覆盖规模 | 28 个插件 / 390 个 PHP 文件 / 23 个双触发钩子文件 |
| 扫描器 | .workbuddy-ai/tmp/audit2.py(原始结果 audit2.json)、.workbuddy-ai/tmp/lint_plugins.php、.workbuddy-ai/tmp/reinclude_test.php |
一、结论摘要
没有发现「一票否决」级的安全漏洞(无反序列化/动态执行、无 SQL 注入、无路径穿越、无 XSS 输出绕过、CSRF 覆盖率 100%)。
问题集中在 1 个功能性致命 BUG、积分/用户组绕过核心封装、以及主题变量化改造不彻底三类。
| 级别 | 确认问题数 | 涉及插件 | 性质 |
|---|---|---|---|
| P0 致命 | 2 | 2 | 1 个功能 100% 不可用 + 1 个零内联红线 |
| P1 高危/规范 | 121 | 18 | 核心库直写 6、主题硬编码 105、内联 5、i18n 1、结构 3、死钩子 1 |
| P2 改进 | 119 | 17 | 旧变量/主色硬编码、直查核心表、风格偏差等 |
| 合计 | 242 | 20 | —— |
扫描器原始命中 285 条,经逐条源码复核剔除 43 条误报(占位符变量
{$ph}、注释内关键词、var(--mn-*, #fff)合法 fallback、PDO::quote()已转义、白名单in_array守卫等),下表为复核后确认值。
最需要立刻处理的两件事:
plugins/invite/InviteController.php:46—— 邀请码生成功能 100% 报 500($db->begin()调用了不存在的方法)。plugins/user_level/views/admin.php:273—— 后台按钮使用内联onclick,违反零内联 P0 红线。
二、审计范围与方法
2.1 检查维度(对应规范章节)
| 维度 | 规范依据 | 检查内容 |
|---|---|---|
| 目录与元数据 | §二、§三、§11.3 | 一目录一插件、残留文件、plugin.json 合法性与必填字段、hooks 路径存在性与分隔符、permissions 声明、plugins_cache.json 一致性 |
| 安全红线 | §一、§一五、§11.1 | 反序列化/动态执行、绝对路径、渲染钩子中断、上传白名单、原子扣减、IN(...) 占位符、PDO 属性、AJAX CSRF、SQL 注入、路径穿越、开放重定向、IDOR |
| 控制器 | §五 | 基类继承、requireLogin()、404 处理、冗余 require_once、静态调用核心 Model |
| 视图与资源 | §六、§七 | section() 组织、$this->e() 转义、$this->url()/BASE_PATH、零内联(<script>/on*/style="")、?v=filemtime |
| 语言包 | §八 | 三语文件齐备、键集合与键序一致、前缀规范、引用键 vs 字典全量比对 |
| 数据库 | §十 | 独立库、核心表访问边界、事务写法、SQLite DDL 语法 |
| 钩子 | §九 | 钩子名合法性、死钩子、双触发钩子顶层符号、渲染钩子约束、导航钩子字号约定 |
| 主题适配 | §11.7 | --mn-* 变量化、硬编码颜色、花括号配对 |
| 性能 | §11.4 | 渲染钩子内同步外部 IO、每页 hook 查询 |
2.2 审计手法(可复现)
# ① 静态扫描(词法预处理:剥离注释与字符串,避免注释内关键词误报)
C:/Users/Administrator/.workbuddy-ai/binaries/python/versions/3.13.12/python.exe .workbuddy-ai/tmp/audit2.py
# ② PHP 语法校验(单进程 token_get_all + TOKEN_PARSE,等价 php -l,390 文件约 2s)
/d/phpstudy_pro/Extensions/php/php8.2.9nts/php.exe .workbuddy-ai/tmp/lint_plugins.php
# ③ 双触发钩子双 include 回归(§9.4 ③;必须带框架引导,否则类未加载会产生假失败)
/d/phpstudy_pro/Extensions/php/php8.2.9nts/php.exe .workbuddy-ai/tmp/reinclude_test.php
# ④ 顶层符号静态普查(§9.4 ①)
grep -rnE '^[ \t]*(abstract |final )?(function|class|interface|trait) [A-Za-z_]' plugins/*/hook/方法学提示:规范 §9.4 ③ 给出的
php -r 'include $f; include $f;'不带框架引导,实测会对 12 个正常钩子文件报Class "app\Helpers\I18n" not found之类的假失败(类未自动加载,与双 include 无关)。必须改为先require app/Core/Autoloader.php再 include,才测得出真实结论(本报告结论基于带引导版本)。
三、问题分级定义
| 级别 | 定义 | 处置要求 |
|---|---|---|
| P0 致命 | 违反 §一五 八条红线,或导致功能完全不可用 | 立即修复,禁止发布 |
| P1 高危/规范 | 违反 §11.1~§11.3 明列红线;绕过核心封装;主题适配失效;残留/死钩子 | 本迭代内修复 |
| P2 改进 | 风格偏差、可读性/一致性、潜在退化风险 | 排期优化 |
四、P0 致命问题(2 条)
P0-1 🔴 invite 邀请码生成功能 100% 报 500
| 项 | 内容 |
|---|---|
| 位置 | plugins/invite/InviteController.php:46 |
| 代码 | $db->begin();(其中 $db = self::db() → \app\Helpers\Plugin::db('invite')) |
| 违反 | §10.2「$db->begin() 不存在——原生 PDO 只有 beginTransaction()」 |
证据链:
InviteController.php:20-22——public static function db(): \PDO { return \app\Helpers\Plugin::db('invite'); }→ 返回原生 PDO;InviteController.php:30——$db = self::db();;InviteController.php:46—— 在else分支(即积分足够、即将生成邀请码)中调用$db->begin();;- 实测:
$ php -r '$p=new PDO("sqlite::memory:"); try { $p->begin(); } catch (\Throwable $e) { echo get_class($e).": ".$e->getMessage(); }'
Error: Call to undefined method PDO::begin()影响:抛出的 Error 不是 Exception,InviteController.php:58 的 catch (\Exception $e) 捕不到 → 未捕获致命错误 → 用户每次点击「生成邀请码」都得到 500 页面(积分不足时因走 if 分支反而正常,掩盖了问题)。
修复建议:
// plugins/invite/InviteController.php:46
- $db->begin();
+ $db->beginTransaction();
// :58 catch 内同时补 inTransaction 守卫(见 P2 项 ROLLBACK_NO_GUARD)
} catch (\Exception $e) {
- $db->rollback();
+ if ($db->inTransaction()) { $db->rollBack(); }同类排查结论:全仓
->begin()共 9 处,其中app/Controllers/PostController.php:133、ThreadController.php:132、app/Helpers/Points.php:222、Permission.php:331、app/Models/Blog.php:124、User.php:609以及red_packet的 3 处,接收者均为app\Core\Database(该类确有此方法,Database.php:130且带嵌套事务保护)→ 仅 invite 这一处是真实缺陷。
P0-2 🔴 user_level 后台按钮使用内联 onclick
| 项 | 内容 |
|---|---|
| 位置 | plugins/user_level/views/admin.php:273 |
| 代码 | <button type="submit" class="btn btn-secondary" onclick="return confirm('<?php echo $this->e($this->t('plugin.user_level.backfill_confirm')); ?>');"> |
| 违反 | §7.3 P0 级第 2 条「禁止内联 on 事件属性,一律在 assets/script.js 内 addEventListener 绑定;传参走 data- 属性」 |
影响:该行文本虽已用 $this->e() 转义(无 XSS),但内联事件属性违反零内联红线,且受 CSP script-src 策略限制时确认弹窗会失效。
修复建议(对齐 announcements / medal / content_review 的既有整改方式):
// assets/script.js
document.querySelectorAll('[data-ul-confirm]').forEach(function (el) {
el.addEventListener('click', function (e) {
if (!confirm(el.dataset.ulConfirm)) e.preventDefault();
});
});五、P1 高危 / 规范问题(121 条)
5.1 核心库直写 —— 绕过 Points / User 封装(6 处)
违反 §11.8-1「禁止直写核心库用户/积分表」。绕过封装会同时丢失:积分日志(points_log)、等级重算、用户缓存失效、points_award_after / points_deduct_after 钩子。
| 插件 | 位置 | 问题 | 影响 |
|---|---|---|---|
daily_checkin | CheckinController.php:218 | UPDATE users SET points = COALESCE(points,0) + :points | 且全插件未写 points_log → 签到积分无任何日志、无等级联动、无钩子 |
quiz_duel | QuizDuelController.php:307 | UPDATE users SET points = points + :p(发奖) | 手工补写 points_log,但等级/缓存/钩子缺失 |
quiz_duel | QuizDuelController.php:327 | UPDATE users SET points = points + :p(退款) | 同上 |
red_packet | RedPacketController.php:311 | UPDATE users SET points = points + :p(抢红包) | 同上 |
red_packet | RedPacketController.php:421 | UPDATE users SET points = points + :p | 同上 |
points_mall | Plugin.php:508 | (new \app\Models\User())->execute('UPDATE users SET group_id = :gid ...') | 虽经 Model 转发,但仍是裸 SQL,未走 User::updateGroupId()(§11.8-1 明列 mod_system 同款写法曾被审计标记) |
修复建议:
// 积分增加
\app\Helpers\Points::award($userId, $points, $reason, $refId, $refType);
// 积分扣减(原子、防负)
\app\Helpers\Points::deduct($userId, $points, $reason, $refId, $refType); // 返回 bool
// 用户组变更
(new \app\Models\User())->updateGroupId($uid, $groupId);
quiz_duel:304注释自述「避免Points::award的级别计算可能导致异常」——属已知取舍,但应按规范改为「Points::award外层 try/catch 兜底」而非绕过封装;invite:54用Points::award($userId, -$pointsCost, ...)传负数扣分,应改用Points::deduct()。
5.2 主题硬编码颜色 —— 暗夜星辰主题下失读(105 处 / 15 个插件)
违反 §11.7。这是本次审计数量最多的一类问题:核心主题只定义 :root 变量,不为插件类补样式,硬编码色值在深色主题下表现为「白卡片 + 浅色字」不可读。
| 插件 | 白色背景 | 深色文字 | 小计 |
|---|---|---|---|
red_packet | 7 | 20 | 27 |
icon_generator | 10 | 15 | 25 |
code_repo | 7 | 10 | 17 |
medal | 2 | 3 | 5 |
friend_links | 2 | 3 | 5 |
seo | 0 | 4 | 4 |
announcements | 2 | 2 | 4 |
ai_assistant | 3 | 0 | 3 |
content_review | 3 | 0 | 3 |
mod_system | 1 | 2 | 3 |
forum_required_tag | 2 | 0 | 2 |
post_favorite | 0 | 2 | 2 |
qa | 0 | 2 | 2 |
invite | 0 | 1 | 1 |
user_profile | 1 | 0 | 1 |
xiuxian | 1 | 0 | 1 |
| 合计 | 41 | 64 | 105 |
典型样本:
/* plugins/announcements/assets/style.css:34 */
.fl-modal { background:#fff; ... }
/* plugins/announcements/assets/style.css:38 */
.fl-modal-field label { ... color:#555; }
/* plugins/code_repo/assets/style.css:57-60 */
.repo-btn-ghost { background:#fff; color:#556; border:1px solid #d9dee8; }
/* plugins/code_repo/assets/style.css:65 */
.repo-btn-disabled { color:#999; }修复模板(fallback 值 = 浅色主题下的目标色,保证浅色零副作用):
.fl-modal { background: var(--mn-bg-card, #fff); }
.fl-modal-field label { color: var(--mn-text-secondary, #555); }
.repo-btn-ghost { background: var(--mn-bg-card, #fff); color: var(--mn-text-secondary, #556); border: 1px solid var(--mn-border, #d9dee8); }
.repo-btn-disabled { color: var(--mn-text-muted, #999); }验证(§11.7 发布前必做):
grep -nE 'background: *#fff|color: *#[0-9a-fA-F]{3,6}' plugins/{插件名}/assets/style.css # 应零命中
# 实机切换「暗夜星辰」主题走查:卡片 / 文字 / 按钮 / 结果框 / 提示框5.3 零内联违规(5 处)
| 插件 | 位置 | 问题 | 规范条款 |
|---|---|---|---|
draft_saver | hook/layout_head_end.php:31 | echo '<script>window.FlintDraftUserId=' . $userId . ';</script>'; | §7.3 P0-1(内联 <script> 块) |
forum_required_tag | views/admin.php:143 / 171 / 181 | style="background:<?php echo $this->e($_c); ?>;" 动态色值直接写 background 属性 | §7.3 P1-4(应改 CSS 变量注入 style="--frt-bg:#xxx" + class 内 var()) |
third_party_login | views/oauth_bind.php:55 | style="padding:10px;" 静态样式 | §7.3 P1-3(应收敛为 class 或核心 mn-p- 工具类) |
draft_saver 修复模板(对齐 §7.3 P1-6「i18n 文案注入 JS」的 data 属性承载方案):
// hook/layout_head_end.php
echo '';
// assets/draft.js
const cfg = document.getElementById('flintDraftCfg');
window.FlintDraftUserId = cfg ? Number(cfg.dataset.userId) : 0;5.4 语言键缺失(1 处)
| 插件 | 键 | 位置 | 说明 |
|---|---|---|---|
code_audit | plugin.code_audit.truncated | views/report.php:64 | 三语字典(zh/en/zh_tw 各 43 键)均无此键 → 报告页在 truncated 为真时直接显示键名原文 |
修复:三个语言包同步补键(键序保持一致,UTF-8 无 BOM + LF):
'plugin.code_audit.truncated' => '结果已截断,仅显示前若干条', // zh.php
'plugin.code_audit.truncated' => 'Results truncated; only the first entries are shown.', // en.php
'plugin.code_audit.truncated' => '結果已截斷,僅顯示前若干筆', // zh_tw.php复核说明:扫描器另报 6 个插件共 6 条 i18n 缺键,逐键溯源后确认全部为动态拼接前缀误报(
plugin.mod_system.cat.{类型}、plugin.task_center.seed.、plugin.user_level.lv{N}、plugin.ai_assistant.reply_type_、plugin.points_mall.type_、plugin.third_party_login.fld_等),对应键族在字典中均齐备。
5.5 结构与元数据(4 处)
| 插件 | 位置 | 问题 | 规范条款 |
|---|---|---|---|
ai_assistant | config.php.bak(6751 B,2026-08-18,早于 config.php 的 08-27) | 插件目录内残留备份文件 | §二「禁止在插件目录放 .bak/.revert/.tmp 等残留文件」 |
edit_info | hook/post_edit_after.php(catch 内 CREATE TABLE IF NOT EXISTS edit_history) | 业务钩子内建表 | §11.3-16「建表放 activate() + init_after(惰性兜底)」 |
edit_info | hook/thread_edit_after.php(同上) | 同上 | 同上 |
post_favorite | plugin.json:13 "nav_pc_user_links": "hook/nav_pc_user_links.php" | 注册死钩子(核心 app/ 与 index.php 中已无任何触发点) | §9.2「新插件不要再注册此钩子」 |
说明:
ai_assistant/config.php.bak删除即可(config.php为现行版本)。edit_info的两处属「表不存在则建表后重试」的惰性兜底,与init_after的定位相同,但按规范应把 DDL 上移到activate()(或至少init_after),业务钩子只做插入。post_favorite的钩子文件内容已改为return;(空转),删除plugin.json中的注册项即可;注意改hooks清单后必须重建plugins/plugins_cache.json(§11.5-30)。
六、P2 改进项(119 条)
| 类别 | 数量 | 涉及插件 | 说明与建议 |
|---|---|---|---|
| 旧版兼容变量 | 55 | ai_assistant(10)、icon_generator(18)、friend_links(7)、post_favorite(6)、user_profile(13)、content_review(1) | 使用 --text-dark / --primary-color / --border-color。这三个变量核心仍有定义(modern.css 各 9 处),属历史兼容;规范 §11.7 要求新代码一律 --mn-text / --mn-primary / --mn-border。建议逐步替换。 |
| 硬编码主色 | 17 | icon_generator | #0066cc 应改 var(--mn-primary)(assets/style.css:298 起 17 处)。 |
直查 users 表 | 16 | medal(5)、mod_system(4)、content_review(2)、ai_assistant(1)、daily_checkin(1)、task_center(1)、user_level(1)、user_profile(1) | §10.1 要求走 User::batchGetNames() / batchGetUsers()(请求级缓存,避免 N+1 与核心库压力)。 |
直读写 blogs/blog_comments | 7 | mod_system(5)、user_profile(2) | 实测两表仍活跃(blogs 16 行、blog_comments 5 行;退役的是 threads/posts)。mod_system 多数已包在 (new \app\Models\Blog())->queryOne(...) 内,仅 AdminController.php:301 / FrontController.php:239 直接走 Database::fetchOne;建议统一经 Model。 |
activate() 用 exec() 而非 ensureSchema() | 6 | ai_assistant、content_review、invite、medal、red_packet、user_level | 功能等价(Plugin::ensureSchema() 实现就是 self::db($name)->exec($ddl),app/Helpers/Plugin.php:86-89),仅风格不一致,属低优先。 |
| 视图内硬编码 URL | 5 | seo(3)、content_review(1)、post_favorite(1) | §11.2-8 要求拼 BASE_PATH,否则二级目录部署 404。 |
IN (...) 用 intval + implode | 3 | content_review、medal、post_favorite | 安全(array_map('intval', ...) 已保证不可注入),但规范 §一五 红线 6 要求「intval 强转 + 生成占位符」。建议统一改 array_fill 占位符写法。 |
catch 内 rollBack() 无 inTransaction() 守卫 | 3 | invite(1)、red_packet(2) | §10.2 明确:若事务未开启,rollBack() 会抛 PDOException: There is no active transaction 并逃逸出 catch → 500(前端 res.json() 解析失败)。 |
| 命名空间风格 | 3 | user_level | Plugin\user_level 非规范驼峰 Plugin\UserLevel。PHP 类名大小写不敏感,功能无影响(含 Plugin::uninstall() 的 class_exists("\Plugin\user_level\Plugin") 也能命中),纯风格问题。 |
| 内联数据块 | 2 | seo、forum_required_tag | <script type="application/json"> / ld+json。非可执行逻辑且已用 JSON_HEX_TAG 防闭合,属安全写法;规范未明确认可,建议改 data-* 属性承载(与 §7.3 P1-6 一致)。 |
| 语言键前缀 | 1 | icon_generator | 70 个键用 plugin.icon_gen. 而非 plugin.icon_generator.。无功能影响(插件语言包按前缀合并,不与核心冲突),但违反 §八「键名统一 plugin.{插件名}.{语义} 前缀」。 |
未实现 activate() | 1 | xiuxian | Plugin.php 有 ensureSchema() 与 ddl() 但无 activate() → 核心激活流程会因 method_exists 为假而跳过建表,实际依赖 init_after 兜底。建议补 activate()。 |
七、分插件总览
| 插件 | 版本 | 启用 | 文件 | 钩子 | 前/后台 | permissions | P0 | P1 | P2 |
|---|---|---|---|---|---|---|---|---|---|
ai_assistant | 1.0.0 | ✅ | 49 | 5 | –/✅ | net:fetch, route:admin | 0 | 4 | 12 |
announcements | 1.2.0 | ✅ | 16 | 3 | –/✅ | route:admin, system:settings | 0 | 4 | 0 |
code_audit | 1.0.0 | ⛔ | 29 | 2 | –/✅ | route:admin | 0 | 1 | 0 |
code_repo | 1.0.0 | ⛔ | 19 | 4 | –/✅ | route:admin, system:settings | 0 | 17 | 0 |
content_review | 2.0.0 | ⛔ | 27 | 9 | ✅/✅ | route:admin, system:settings | 0 | 3 | 6 |
daily_checkin | 1.0.0 | ✅ | 20 | 5 | –/– | system:settings | 0 | 1 | 1 |
dice | 1.0.0 | ⛔ | 26 | 5 | –/– | (未声明) | 0 | 0 | 0 |
draft_saver | 1.0.0 | ✅ | 6 | 1 | –/– | [] | 0 | 1 | 0 |
edit_info | 1.5.0 | ✅ | 13 | 4 | –/– | (未声明) | 0 | 2 | 0 |
floor_reply | 1.1.0 | ✅ | 21 | 6 | ✅/✅ | route:admin, system:settings | 0 | 0 | 0 |
forum_required_tag | 1.1.0 | ✅ | 23 | 7 | ✅/✅ | route:admin | 0 | 5 | 1 |
friend_links | 2.0.0 | ✅ | 19 | 4 | ✅/✅ | route:admin, system:settings | 0 | 5 | 7 |
icon_generator | 1.0.0 | ⛔ | 11 | 2 | ✅/– | (未声明) | 0 | 25 | 36 |
invite | 1.5.0 | ⛔ | 16 | 6 | –/– | (未声明) | 1 | 1 | 2 |
medal | 1.0.0 | ✅ | 24 | 9 | –/– | route:admin | 0 | 5 | 7 |
mod_system | 1.0.0 | ✅ | 30 | 11 | ✅/✅ | route:admin, system:settings | 0 | 3 | 9 |
points_mall | 1.0.0 | ✅ | 22 | 7 | ✅/✅ | route:admin | 0 | 1 | 0 |
post_favorite | 1.0.0 | ✅ | 18 | 5 | –/– | (未声明) | 0 | 3 | 8 |
qa | 1.0.0 | ⛔ | 29 | 8 | –/✅ | route:admin, system:settings | 0 | 2 | 0 |
quiz_duel | 1.0.0 | ⛔ | 21 | 4 | –/– | (未声明) | 0 | 2 | 0 |
red_packet | 1.8.0 | ✅ | 20 | 6 | –/– | (未声明) | 0 | 29 | 3 |
seo | 1.5.0 | ✅ | 18 | 5 | ✅/✅ | route:admin, system:settings | 0 | 4 | 4 |
single_page | 1.0.0 | ✅ | 19 | 4 | ✅/✅ | route:admin | 0 | 0 | 0 |
task_center | 1.0.0 | ✅ | 26 | 11 | ✅/✅ | route:admin | 0 | 0 | 1 |
third_party_login | 1.0.0 | ⛔ | 22 | 5 | ✅/✅ | route:admin, net:fetch | 0 | 1 | 0 |
user_level | 1.0.0 | ✅ | 28 | 11 | ✅/✅ | route:admin, system:settings | 1 | 0 | 5 |
user_profile | 1.0.0 | ✅ | 16 | 4 | ✅/– | route:admin | 0 | 1 | 16 |
xiuxian | 1.0.0 | ✅ | 24 | 4 | ✅/– | [] | 0 | 1 | 1 |
| 合计 | 390 | 2 | 121 | 119 |
零问题插件:dice、floor_reply、single_page(3 个)。
八、通过项(合规确认)
以下维度经全量扫描 + 抽样复核,未发现任何违规:
8.1 安全(8 条最高级别红线)
| 检查项 | 结果 |
|---|---|
反序列化 / 动态执行(unserialize/eval/assert/create_function/preg_replace /e) | ✅ 0 处 |
$_SERVER['DOCUMENT_ROOT'] 绝对路径 | ✅ 0 处 |
渲染钩子内 die()/exit()/header() | ✅ 0 处(唯一命中在 seo/hook/layout_head_end.php:6 的注释中,已排除) |
上传弱随机名 / 缺白名单(move_uploaded_file) | ✅ 0 处 |
IN (...) 拼接用户输入(真实注入面) | ✅ 0 处(命中项经溯源均为占位符 / intval / quote() / 白名单守卫) |
覆盖 Plugin::db() 的 PDO 属性 | ✅ 0 处 |
| AJAX / 表单 CSRF | ✅ 0 缺失 —— 逐方法扫描全部含 $_POST 且含写操作的控制器方法,100% 调用 Csrf::verify/verifyOrDie |
路径穿越(文件函数直用 $_GET/$_POST/$_FILES) | ✅ 0 处 |
| 静态调用核心 Model 实例方法 | ✅ 0 处(ai_assistant/cli/diag_thread.php 已正确用 (new \app\Models\Thread())->find()) |
| 后台鉴权 | ✅ 17 个插件后台控制器全部 use app\Controllers\Admin\BaseController(统一走 Auth::isAdmin() + 二次验证门禁) |
| 开放重定向 | ✅ 0 处(user_level/hook/route_before_dispatch.php 的 ulGateBackUrl() 做了同源 + BASE_PATH 校验) |
8.2 结构、元数据与钩子
| 检查项 | 结果 |
|---|---|
| PHP 语法 | ✅ 390 个文件,0 错误(token_get_all + TOKEN_PARSE) |
plugin.json | ✅ 28/28 存在、合法 JSON、无 BOM;name/version/author/description/activated/hooks 必填字段齐备 |
| hooks 路径 | ✅ 全部使用 / 分隔;指向文件全部存在;无未知钩子名 |
plugins_cache.json 一致性 | ✅ 28 个插件的 hooks 清单与 plugin.json 零差异 |
| 死钩子 | ⚠️ 仅 post_favorite 1 处(见 P1) |
| 双触发钩子双 include 回归 | ✅ 23 个文件全部输出一致(23/23),0 处 Cannot redeclare |
| 钩子顶层符号声明 | ✅ 仅 user_level/hook/route_before_dispatch.php:46 1 处,且是规范 §9.4 的正例参考(function_exists 包裹 + 置于文件顶部) |
admin_sidebar_links 注册 | ✅ 0 处(符合「新插件一律不注册,改用 admin_url」) |
| 导航钩子图标字号 | ✅ nav_plugin_links 全部 16px(13 处);nav_user_menu_items 全部 14px(5 处) |
nav_plugin_links 高亮判定 | ✅ 11/13 正确用 __nav_uri;2 处偏差(qa、xiuxian 读 $_SERVER['REQUEST_URI'])——规范 §9.3 已记录该偏差,二级目录部署下易失效,建议后续统一 |
8.3 数据库与 i18n
| 检查项 | 结果 |
|---|---|
插件独立库(Plugin::db()) | ✅ 全部使用独立库,无写全局 business.sqlite |
| SQLite DDL 语法 | ✅ 0 处 MySQL 写法(AUTO_INCREMENT/VARCHAR(n)/ENGINE=InnoDB/SHOW TABLES LIKE 等;ai_assistant/cli/ 中的 datetime('now','localtime') 是合法 SQLite 函数) |
读写退役表 threads/posts | ✅ 0 处(均已迁移至 main_index.topic_index) |
| 三语语言包 | ✅ 26 个有 lang/ 的插件,zh/en/zh_tw 键数完全相同、键集合零差异 |
| 语言包编码 | ✅ 无 BOM |
| 引用键 vs 字典全量比对 | ⚠️ 仅 1 处真实缺失(code_audit.truncated,见 P1) |
九、修复优先级建议
第 1 批 —— 立即(P0,2 条)
plugins/invite/InviteController.php:46→beginTransaction();:59catch 内补inTransaction()守卫。plugins/user_level/views/admin.php:273→ 内联onclick改data-ul-confirm+assets/script.js。
第 2 批 —— 本迭代(P1 核心库直写 6 条)
daily_checkin/quiz_duel/red_packet的 5 处积分直写改Points::award()/Points::deduct();daily_checkin同时补齐points_log。points_mall/Plugin.php:508改User::updateGroupId()。
⚠️ 这几处涉及积分与用户组,改动前务必按 §11.8-1 核对
Points接口(award/deduct均带条件原子更新与rowCount()检查),并在改动后回归「积分扣减无负数、等级联动、points_deduct_after钩子触发」三项断言。
第 3 批 —— 本迭代(P1 其余 115 条)
- 主题硬编码 105 处:按 §11.7 对照表批量替换为
var(--mn-*, fallback),改完必须双主题(浅色 + 暗夜星辰)走查。 - 零内联 5 处(
draft_saver1、forum_required_tag3、third_party_login1)。 code_audit补plugin.code_audit.truncated三语键(注意键序一致、UTF-8 无 BOM + LF)。- 删
ai_assistant/config.php.bak;edit_info两处 DDL 上移;post_favorite移除死钩子注册并重建plugins_cache.json。
第 4 批 —— 排期(P2 119 条)
- 旧变量 55 处 →
--mn-*;icon_generator主色 17 处 →--mn-primary。 - 直查
users表 16 处 →User::batchGetNames()/batchGetUsers()。 - 其余风格项(
ensureSchema、占位符、inTransaction守卫、命名空间、xiuxian::activate())随相关文件改动时顺带修正。
发布前必做(§11.5 / §11.7)
# ① 改过 hooks 清单的插件 → 重建缓存
# 删除 plugins/plugins_cache.json 或调用 \app\Helpers\Plugin::refresh()
# ② 改过 nav_* / layout_head_end / footer_links → 清前台页面缓存
# \app\Helpers\PageCache::invalidate() (注:只覆盖列表页且仅游客)
# ③ 零内联复查(应零命中,十、附录
10.1 审计产物
| 文件 | 说明 |
|---|---|
.workbuddy-ai/tmp/audit2.py | 静态扫描器(词法预处理 + 9 大维度规则) |
.workbuddy-ai/tmp/audit2.json | 扫描原始结构化结果(含全部命中项与文件行号) |
.workbuddy-ai/tmp/lint_plugins.php | PHP 语法校验(单进程) |
.workbuddy-ai/tmp/reinclude_test.php | 双触发钩子双 include 回归 |
.workbuddy-ai/tmp/confirmed.txt | 剔除误报后的确认清单 |
10.2 扫描器误报成因(供后续复用参考)
| 误报类型 | 成因 | 修正方式 |
|---|---|---|
| 注释内关键词 | 正则未剥离 //、/ / | 引入 PHP 词法预处理,注释与字符串分别置空 |
| SQL 占位符 | IN ({$ph}) 中 $ph = implode(',', array_fill(0, n, '?')) | 对 SQL 内插变量做赋值溯源:含 array_fill+? → 安全 |
| 已转义拼接 | ->quote($x)、array_map('intval', ...) | 识别 quote/intval 即降级 |
| 白名单守卫 | if (!in_array($table, [...], true)) return; 后再内插 | 人工复核确认(正则难以自动判定) |
| 合法 fallback | var(--mn-bg-card, #fff) 中的 #fff 是规范要求的浅色兜底值 | 排除 var(--mn-*, #hex) 同行命中 |
| 变量名子串 | $datetime 命中 DATETIME(MySQL 类型检测) | 加 (?<![A-Za-z_$]) 前置否定 + (?!\s*\() 排除函数调用 |
| 动态 i18n 键 | $this->t('plugin.x.lv' . $n) → 提取出 plugin.x.lv | 逐键溯源到字典中的键族 |
10.3 已知但未计入的问题(规范 §11 已登记,本次未重复发现)
mod_system的users.status不校验、写路径零事务;AuditLog无痕、外部正文锁不一致;pagination()出口不转义、搜索页type反射 XSS;fr_replies.thread_id有 1 处既有不一致(id=28post=1425 存 76,实际主题 81)—— 已记录未改。
以上均属核心层问题,不属于插件规范范畴,故未计入本报告问题总数。