本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/docs/vn_postname插件审计报告_2026-09-23.md。
vn_postname(马甲发帖)插件审计报告
- 审计日期:2026-09-23
- 审计对象:
plugins/vn_postname/(v1.1.0,plugin.json声明版本) - 审计依据:
docs/插件开发规范.md(2026-09-22 版) - 审计方式:只读审计 —— 读源码 + 实测取证,未改动任何插件文件
- 结论:不规范,需修复。共 17 项问题(P0×3 / P1×4 / P2×4 / P3×6),其中 P0-2 为设计级安全缺陷。
一、结论速览
| 级别 | 数量 | 一句话 |
|---|---|---|
| P0 | 3 | ① 生命周期类名推导失效(卸载留孤儿表)② 身份冒用 —— 可用 @@管理员名 以管理员身份发帖 ③ 发帖提示钩子是空实现,用户不知道有此功能 |
| P1 | 4 | ④ 三语语言包缺键 ⑤ init_after 每请求跑 DDL ⑥ 不校验用户封禁状态 ⑦ 自动建号零护栏 |
| P2 | 4 | ⑧ 全文件 CRLF ⑨ 发帖数双计、马甲积分不同步 ⑩ 搜索索引条件性不同步 ⑪ 后台视图双重转义 |
| P3 | 6 | 版本号不同步、uninstall() 无异常保护、strip 留空段落等 |
值得肯定(实测通过,见 §四):钩子名与参数全部核实正确、后台路由挂 admin_route_register、CSRF 生效、不直写核心用户表、双写改写成功、strip_marker 生效、零内联、无残留文件。
最要紧的一条:P0-2。当前后台配置 allowed_groups = [](仅管理员可用),风险被默认配置临时兜住;但插件卖点正是"按用户组授权",一旦管理员勾选任何普通用户组,该组所有人即可冒用管理员身份发帖。
二、插件概况
plugins/vn_postname/
├── plugin.json 762B version 1.1.0, hooks×5, permissions[route:admin]
├── Plugin.php 16518B 配置存取 / 身份解析 / 作者改写 / 日志
├── AdminController.php 3045B 后台设置页(继承 Admin\BaseController)
├── hook/
│ ├── init_after.php 建表兜底
│ ├── admin_route_register.php 后台路由
│ ├── thread_create_after.php 发帖后改写作者
│ ├── post_create_after.php 回帖后改写作者
│ └── post_create_extra.php 发帖表单提示 ← 空实现
├── views/admin.php 6893B
├── lang/{zh,en,zh_tw}.php
├── assets/style.css
└── data/vn_postname.sqlite + .htaccess + web.config(核心自动生成)工作机制:核心在 PostController / ThreadController 以「当前登录用户」为作者写入,只提供 thread_create_after / post_create_after 两个"创建后"钩子(不传正文)。插件在 after 钩子里读回刚建的帖子 → 解析 @@用户名 → 解析/创建马甲 → 改写作者(main_index 索引 + 分桶真相源双写)→ 按需去掉 @@ 标记。
核心 API 依赖核查(全部存在,签名匹配):
| 调用 | 位置 | 结论 |
|---|---|---|
User::find() | app/Core/Model.php:24 | ✓ 继承自基类,SELECT * 含 role/group_id |
User::findByUsername() | app/Models/User.php:27 | ✓ |
User::create(u,p,e) | app/Models/User.php:489 | ✓ 不写 role → 落表默认值,不会造管理员 |
User::updateAvatar() / defaultAvatarPath() | :183 / :205 | ✓ |
User::incrementPostCount() | app/Models/User.php:156 | ✓ |
User::batchGetNames() | app/Models/User.php:351 | ✓ 静态 |
Thread::find() / update() | app/Models/Thread.php:187 / :440 | ✓ update 白名单含 title/content |
Post::find() / update() | app/Models/Post.php:218 / :328 | ✓ |
Database::getInstance()->fetchAll() | — | ✓ 用对了(Database 无 prepare(),此处未误用) |
表结构核查:data/vn_postname.sqlite 实际结构与代码 DDL 完全一致(无手工改动痕迹,§11.21-2 通过);vnpn_log 有 1 行真实记录,说明插件已在生产使用过。
三、问题清单
P0-1 生命周期类名推导失效 → 卸载留孤儿表
规范依据:§4.1 / §11.12 / §11.21-4「目录名含下划线 → 必须在 Plugin.php 末尾注册 class_alias」
证据(实测):
Plugin\vn_postname\Plugin class_exists = false
Plugin\VnPostname\Plugin class_exists = trueapp/Helpers/Plugin.php:699 用 "\\Plugin\\{$name}\\Plugin" 拼主类名,$name = 目录名 vn_postname;实际命名空间是 Plugin\VnPostname。下划线不是大小写差异,class_exists 恒 false → activate() / deactivate() / uninstall() 三方法静默跳过。
影响面(按规范 §4.1 口径):
activate():≈ 无影响 —— 有init_after建表兜底deactivate():无影响(空实现)uninstall():唯一真实后果 —— 卸载后vnpn_config/vnpn_log两张表残留,且完全静默
修法(零核心改动,本机已有 6 个先例:mod_system / vn_block / quiz_duel / third_party_login / points_mall / bitiful_storage):
// Plugin.php 末尾
if (!\class_exists('Plugin\\vn_postname\\Plugin', false)) {
\class_alias(Plugin::class, 'Plugin\\vn_postname\\Plugin', false);
}P0-2 身份冒用:可用 @@管理员用户名 以管理员身份发帖 ★最严重
问题:Plugin::resolveUser()(Plugin.php:175-211)对任意已存在的用户名直接返回其 uid,没有任何"只能用自己的马甲"或"禁止冒用高权限账号"的约束:
$user = (new \app\Models\User())->findByUsername($username);
if ($user && (int)$user['id'] > 0) {
return [(int)$user['id'], false]; // ← 直接采纳,不校验目标身份
}实测证据(只读探测,allowCreate=false,未创建任何账号):
@@flinthub -> uid=1 ← 管理员
@@虚拟化大师 -> uid=2
@@浏览器内核工程师 -> uid=3
@@GitHub潜水员 -> uid=4
@@新手导师 -> uid=5
@@会议演讲者 -> uid=6
不存在(allowCreate=false) -> uid=0风险链:
- 管理员在后台按插件设计勾选「普通会员」(group_id=1)
- 任一普通会员发帖时输入
@@flinthub isAllowed(操作者)= true(组已授权)→resolveUser('flinthub')= uid 1reattributeThread()把topic_index.uid与topic.uid双写为 1- 前台帖子作者显示为管理员,管理员的
post_count也被 +1
这不是配置失误,是插件功能设计的必然结果 —— 插件把"任意用户名"当作可自由指派的马甲池,而系统中「用户名」同时是登录凭据与身份标识。同一机制下,普通用户也可以 @@ 另一个普通用户来栽赃。
当前为何未爆:实测配置 {"enabled":1,"allow_create":1,"strip_marker":1,"allowed_groups":[]} → allowed_groups 为空 → isAllowed() 仅对 role='admin' 返回 true(实测 uid1=true,uid2~6=false)。风险被"仅管理员可用"这个默认值兜住了。
可选修法(需你拍板,涉及产品语义):
| 方案 | 做法 | 代价 |
|---|---|---|
| A(推荐,最小改动) | resolveUser() 增加拒绝名单:目标 role='admin' 或 group_id 属于管理组 → 拒绝 | 保留"马甲"本意,杜绝提权;但用户仍能冒用普通用户 |
| B | 只允许使用自己名下的马甲:马甲表记 owner_id,resolveUser 校验 owner_id === operatorId | 语义最干净,但要加表加字段、改交互(需"创建马甲"入口) |
| C | 仅禁止冒用 role='admin'(等价 A 的简化版) | 与 A 相近,实现更粗 |
| D | 保持现状,仅在后台勾选区加醒目警告 | 零代码改动,风险自担 |
我倾向 A:一行判定即可堵住提权,且不动现有数据模型。但这属于产品语义取舍,我不替你拍板。
P0-3 发帖提示钩子是空实现 → 用户完全不知道有 @@ 功能
规范依据:§9.5「渲染钩子只输出提示,不做重定向/中断」—— 但本钩子什么都没输出。
证据:hook/post_create_extra.php 全文 20 行,末尾(:20)为:
if (!\Plugin\VnPostname\Plugin::isAllowed((int)$user['id'])) {
return;
}
// ← 文件到此结束,无任何 echo文件头注释却写着「渲染钩子:只输出提示,不做重定向/中断」。
实测(php -S + 管理员会话):
GET /post/new -> http=200 size=34983
钩子锚点 id="file-name" 存在 : 1 ← 证明钩子确实在该页触发
马甲提示出现次数 : 0 ← 钩子输出为空连带问题:en.php / zh_tw.php 里都写好了 plugin.vn_postname.form_hint 文案(「提示:内容或标题里输入 @@使用者名称 即可用该身分发佈」),只有 zh.php 没有 —— 明显是"提示没写完就搁置"留下的痕迹(见 P1-1)。
另::10 的 if (($target_type ?? '') === 'blog') 是死代码 —— 规范 §9.1 :676 明确 post_create_extra 注入参数为「无」,且全仓库只有 app/Views/post/create.php:72 一处挂载(已 grep 核实)。$target_type 永不存在,该判断恒为 false。
修法:补上提示输出(<div class="form-hint"> + I18n::get('plugin.vn_postname.form_hint')),并删掉死代码分支;同时补 zh.php 的键(P1-1)。
P1-1 三语语言包键集合不一致
规范依据:项目 i18n 基线(三语键集合零差异 + 键序一致);规范 §8 亦要求"建议同步"
实测:
zh=22 en=23 zh_tw=23
zh - en = [] en - zh = ['plugin.vn_postname.form_hint']
zh - zh_tw = [] zh_tw - zh = ['plugin.vn_postname.form_hint']
en - zh_tw = [] zh_tw - en = []唯一缺键:plugin.vn_postname.form_hint(中文简体缺失)。
当前未显现的原因:该键当前没有任何视图引用(因为 P0-3 的钩子是空的)。一旦修 P0-3,中文用户就会在发帖页看到键名原文 plugin.vn_postname.form_hint。两条必须一起修。
P1-2 init_after 每请求跑 DDL,无戳文件门控
规范依据:§11.15 明文「别在控制器每请求跑 DDL;ensureSchema() 本身无缓存门控,要在插件侧加戳文件 / 会话守卫」
证据:hook/init_after.php 全文 9 行,无条件执行两条 DDL:
\app\Helpers\Plugin::ensureSchema('vn_postname', \Plugin\VnPostname\Plugin::ddlConfig());
\app\Helpers\Plugin::ensureSchema('vn_postname', \Plugin\VnPostname\Plugin::ddlLog());app/Helpers/Plugin.php:86-89 确认 ensureSchema() 无门控:
public static function ensureSchema(string $name, string $ddl): void
{
self::db($name)->exec($ddl); // 直接 exec,无 marker 判断
}实测开销(200 次循环均值):
ensureSchema ×2 每请求: 0.175 ms + 0.144 ms = 0.319 ms
(对照)戳文件 is_file: 0.142 ms单看耗时差距不大(~0.18ms),但真正的代价是:每个前台请求都对插件 SQLite 库执行一次 DDL(CREATE TABLE IF NOT EXISTS 在 SQLite 中走 schema 写事务),在并发下会争抢该库写锁。
修法(本机既有范式,points_mall/hook/init_after.php 同款):
$__stamp = __DIR__ . '/../../../protected/.vn_postname_table_cache';
$__dbFile = __DIR__ . '/../data/vn_postname.sqlite';
if (is_file($__stamp) && is_file($__dbFile)) { return; }
try {
\Plugin\VnPostname\Plugin::activate();
@file_put_contents($__stamp, '1', LOCK_EX);
} catch (\Throwable $e) {
\error_log('vn_postname init error: ' . $e->getMessage());
}多判
$__dbFile存在是必要的:只判戳文件会在"戳文件在、库被删"时永久跳过建表。
P1-3 isAllowed() 不校验用户状态(封禁/禁用)
规范依据:§11.8 核心库访问边界精神(权限判定应基于权威状态)
证据:Plugin.php:148-169,判定链只有两条 —— role === 'admin' 或 group_id 在授权列表:
if ((string)($user['role'] ?? '') === 'admin') return true;
$gid = (int)($user['group_id'] ?? 0);
return $gid > 0 && in_array($gid, $cfg['allowed_groups'], true);未检查 users.status(active / 封禁)。而 user_groups 表里存在 id=5「封禁账户」:
1 普通会员 (is_default=1) 2 VIP会员 3 版主 4 管理员 5 封禁账户风险:若管理员在后台勾选了「普通会员」(id=1),而某用户 status='banned' 但 group_id 仍为 1 → 该封禁用户仍可用马甲发帖(是否真能走到钩子取决于核心发帖入口是否拦截,但插件侧自身没有任何防线)。
当前未显现:实测 users 表 285 行 status 全为 active。
修法:isAllowed() 增加 if (($user['status'] ?? '') !== 'active') return false;
P1-4 自动建号零护栏
规范依据:§11.8「禁止直写核心库用户表」—— 本插件已合规(走 User::create()),但业务护栏缺失。
证据:Plugin.php:191-206,allow_create 开启时对任意 @@用户名 建号:
$password = bin2hex(random_bytes(16));
$email = 'vnpn_' . bin2hex(random_bytes(6)) . '@noreply.local';
$uid = (new \app\Models\User())->create($username, $password, $email);缺失的护栏:
- 无用户名黑名单:可创建
admin/administrator/系统/ 客服类易混淆名(User::create()只校验唯一性,不校验语义) - 无频率限制:单用户可无限建号(无
RateLimiter调用) - 无数量上限:未限制单用户/全站马甲总数
- 不写插件关联表:
user_level插件的ul_users.map_lv(信任等级)不会为新马甲建行 → 马甲在等级体系里缺失;其他以users为外键的插件同理 created_at被设置,但email_verified=0、level/points走表默认 —— 与正常注册流程产出的用户不完全等价
实测已有产出:users 表 id=285 小助手 / vnpn_3dc45b8733bd@noreply.local / avatar=seed_avatars/2026-09/285.svg(头像补齐生效 ✓),post_count=1。
修法建议:至少加用户名黑名单(拒绝与 role='admin' 用户重名、拒绝保留字)+ RateLimiter 节流;关联表补齐可作为后续项。
P2-1 全部 12 个文本文件为 CRLF 行尾
规范依据:§11.19「插件内所有文本文件统一 UTF-8 无 BOM + LF。BOM 与 CRLF 都会被打包 / 卫生门禁判为 FAIL」
实测(Python 数 b"\r\n",规范指定手法):
AdminController.php CRLF= 90 LF= 90 BOM=False
Plugin.php CRLF= 430 LF= 430 BOM=False
plugin.json CRLF= 0 LF= 19 BOM=False ← 唯一 LF
assets/style.css CRLF= 8 LF= 8 BOM=False
hook/admin_route_register.php CRLF=11 LF=11 BOM=False
hook/init_after.php CRLF= 9 LF= 9 BOM=False
hook/post_create_after.php CRLF=15 LF=15 BOM=False
hook/post_create_extra.php CRLF=20 LF=20 BOM=False
hook/thread_create_after.php CRLF=13 LF=13 BOM=False
lang/en.php CRLF=30 LF=30 BOM=False
lang/zh.php CRLF=29 LF=29 BOM=False
lang/zh_tw.php CRLF=30 LF=30 BOM=False
views/admin.php CRLF=126 LF=126 BOM=FalseBOM 全部干净 ✓;CRLF 12/13 文件命中(plugin.json 是 LF)。修法:批量转 LF 后必须用 Python 复核(grep/od 在本环境不可靠)。
注意:
plugins/mod_system/下 4 个文件是规范明文例外(必须保持 CRLF),本插件不在例外清单内。
P2-2 发帖数双计、马甲积分不同步
证据:Plugin.php:297-298(发帖)与 :340(回帖):
// 马甲发帖数 +1(走核心封装;操作者发帖数不回退,避免直写核心 users 表)
(new \app\Models\User())->incrementPostCount($uid);问题:核心在 PostController / ThreadController 里已经给操作者 post_count + 1;插件再给马甲 +1 → 一次发帖,全站发帖总数虚高 1(两个人各记一笔)。
代码注释说"操作者发帖数不回退,避免直写核心 users 表"—— 这个取舍合规(§11.8 禁止直写),但结果是计数失真。
另:核心对发帖调用 Points::award(...) 给操作者加分(ThreadController.php:140),插件未给马甲加积分 → 马甲 points=0(实测 id=285 points=0)→ 等级/积分体系对马甲失效。
修法选项:① 操作者 decrementPostCount() 对冲(走 Model 方法,合规)② 或接受现状并在文档中说明"马甲发帖计入马甲名下,操作者同时计入"。积分是否补给马甲需你拍板(涉及经济体系)。
P2-3 搜索索引条件性不同步
证据链:
app/Controllers/PostController.php:155Search::indexThread($threadId);→:157Plugin::hook('thread_create_after', ...)- 即 索引时标题仍带
@@用户名 - 插件随后
Thread::update(['title' => stripMarker(...)]),而Thread::update()(app/Models/Thread.php:527-530):
$mode = \app\Helpers\Settings::get('queue_mode', 'sync');
if ($mode !== 'sync') {
Queue::push('rebuild_search', ['topic_id' => $id]);
}
// sync 模式:注释说"控制器已同步 Search::indexThread,此处不重复重建也不入队"→ queue_mode='sync' 时索引永不重建,@@用户名 词条残留。
实测(当前部署 queue_mode = cron):
帖子 92 索引词条: [新闻] [马甲] [甲发] [发帖] [帖测] [测试]
全库含 @ 的词条: 0结论:当前 cron 模式下由异步队列兜住,实测索引干净。缺陷为条件性(sync 模式 + 队列未消费时才会显现)。
修法:插件在 Thread::update() 后显式补一次 Search::indexThread($threadId)(幂等,代价可接受,且只在用到 @@ 标记的少数发帖上触发)。
P2-4 后台视图双重转义
证据:views/admin.php:102 与 :111
$tgName = $names[$tgId] ?? $this->e((string)$l['username']); // :102 已转义
...
<?php echo $this->e((string)$tgName); ?> // :111 再转一次后果:当 batchGetNames() 未命中、回落到日志里的 username 时,含 < / & / " 的用户名会被双转义(如 A&B → 显示成 A&B)。命中 $names 的正常分支只转一次 ✓ 正确。
修法::102 去掉 $this->e(),统一在输出点转义(符合项目"赋值时转义"约定的输出点单次转义原则)。
P3 级(6 项,不阻塞功能)
| # | 问题 | 证据 | 影响 |
|---|---|---|---|
| P3-1 | 版本号不同步 | plugin.json = 1.1.0,Plugin.php:7 / AdminController.php:7 头注释 = 1.0.0 | 排查时误导 |
| P3-2 | uninstall() 无异常保护 | Plugin.php:50-51 裸调 self::db()->exec(),无 try/catch(对比 vn_block 修复后的范式) | 单表删除失败即中断,另一表残留 |
| P3-3 | stripMarker 留空段落 | 实测 <p>@@张三</p><p>正文</p> → <p></p><p>正文</p> | 标记独占一段时渲染出多余空行 |
| P3-4 | __nav_active='vn-postname' 是死参数 | app/Views/admin/_sidebar.php 无插件子项;admin_url 仅 app/Views/admin/plugins.php:106 消费 | 无害但无意义 |
| P3-5 | handlePostCreated() 的 $threadId 形参未使用 | Plugin.php:307 收了 int $threadId,方法体内无引用 | 轻微冗余(钩子文件仍正确传参) |
| P3-6 | 规范文档与代码不一致(非插件缺陷) | 规范 §9.1 :691 注「post_create_after 此时仍在外层事务内」,实际 ThreadController.php:144 commit() → :153 if ($replyOk) → :175 hook | 实际在事务外。这条对审计有实质意义:插件在钩子里做跨库写 + 文件 I/O 是安全的,无需按"事务内"约束返工。建议顺手更新规范 |
四、合规项(实测通过)
| 检查项 | 规范依据 | 实测结果 |
|---|---|---|
| 钩子名与参数核实 | §9.1 / §9.5 | ✓ thread_create_after(thread_id,user_id) / post_create_after(post_id,thread_id,user_id) 全部与规范一致 |
| 后台路由挂对钩子 | §5.3 | ✓ 注册在 admin_route_register,route:admin 权限已声明,核心校验通过 |
| CSRF 校验 | §一五 | ✓ 无 CSRF / 错 CSRF 均 403 |
| 未登录访问拦截 | — | ✓ 302 → /login |
| 后台管理员校验 | — | ✓ 继承 Admin\BaseController,构造器自动校验 + 二次验证 |
| 零内联 | §11.4-7 | ✓ `grep -rnE '<style\ |
php -l 语法 | §11.5-32 | ✓ 12/12 通过 |
| 无残留文件 | §二禁止项 | ✓ 无 .bak / .tmp / .revert / .orig |
| 独立库 + 数据目录防护 | §十 | ✓ Plugin::db() 自动生成 .htaccess + web.config |
| 不直写核心用户/积分表 | §11.8-1 | ✓ 全部走 Model 封装(create/incrementPostCount/updateAvatar) |
| 批量取用户名 | §11.8-3 | ✓ AdminController:84 用 User::batchGetNames() |
核心 Model update() 白名单 | §11.8-4 | ✓ Thread::update() / Post::update() 均为重写方法,字段在白名单内 |
| 后台视图区块 | §六 | ✓ section('title') + section('content') |
| 后台 CSS 路径 | — | ✓ __DIR__.'/../assets/style.css' 正确命中(实测 ?v=1786516878,非退化的 v=1) |
| 后台 CSS 硬编码色 | §11.7 | ✓ 有正当理由 —— 后台无 --mn-* 变量(规范 §11.10),文件头注释已明示;未污染前台样式 |
plugins_cache.json 同步 | §11.5-30 | ✓ hooks 清单与 plugin.json 一致 |
| 双写改写正确 | 记忆 §1 | ✓ 实测帖子 92 topic_index.uid = 285(马甲) |
| 头像补齐 | — | ✓ 实测 seed_avatars/2026-09/285.svg |
strip_marker 生效 | — | ✓ 实测 /thread/92 零处 @@ 残留,标题渲染为 [新闻] 马甲发帖测试 |
| 后台页 i18n 无缺键 | §八 | ✓ 实测页面 零处键名原文 |
| 配置 UPSERT | §十 | ✓ ON CONFLICT(id) DO UPDATE 单行幂等 |
user_groups 读法 | §11.8 | ✓ 与 points_mall/AdminController.php:107 完全同款,符合既有范式 |
| 卸载干跑 | §11.5-34 | ✓ 库副本上两表清空(但核心路径因 P0-1 不会真正执行) |
五、实测记录(可复现)
环境:php -S 127.0.0.1:8899(PHP 8.2.9nts)+ 手写管理员会话(admin_verified_at + csrf + csrf_time),curl --noproxy '*'。
| # | 探测 | 结果 |
|---|---|---|
| 1 | class_exists('\Plugin\vn_postname\Plugin') | false ← P0-1 |
| 2 | resolveUser('flinthub', false) | uid=1 ← P0-2 |
| 3 | GET /post/new | 200,id="file-name" 锚点存在,马甲提示 0 处 ← P0-3 |
| 4 | 三语键比对 | zh=22 / en=23 / zh_tw=23,缺 form_hint ← P1-1 |
| 5 | ensureSchema ×2 开销 | 0.319 ms/请求(戳文件对照 0.142 ms)← P1-2 |
| 6 | GET /admin/vn-postname | 200,<title>马甲发帖 - 燧石社区</title>,5 个组复选框,1 条日志 |
| 7 | POST /admin/vn-postname/save(无/错 CSRF) | 403 / 403 ✓ |
| 8 | GET /admin/vn-postname(无会话) | 302 → /login ✓ |
| 9 | GET /thread/92 | 200,作者「小助手」出现 6 次,@@ 残留 0 处 ✓ |
| 10 | 帖子 92 搜索索引词条 | 6 条,零 @ 词条(queue_mode=cron 兜住) |
| 11 | 卸载干跑(库副本) | 两表清空 ✓,临时库已删 ✓ |
| 12 | 零内联 / php -l / 残留文件 | 全 PASS |
探针清理:测试会话(vnpnprobe%)已删;curl 产生的 3 个游客会话已按精确 id 删除(未触碰任何登录态会话);7 个临时文件已删;sessions 表恢复 167 行(与审计前一致);php -S 已停。
另发现(非本次探针,属你已有数据):帖子 id=92「[新闻] 马甲发帖测试」(作者 uid=285)+ 用户 id=285「小助手」+ vnpn_log 1 行 —— 是你自己测试马甲功能留下的,我未改动。是否清理请你决定。
六、修复优先级建议
| 批次 | 项 | 理由 |
|---|---|---|
| 第 1 批(必做) | P0-1 class_alias、P0-3 补提示输出、P1-1 补 zh.php 键 | 三条互相关联,改动小、风险低,且 P0-3 + P1-1 必须一起改 |
| 第 2 批(需你拍板) | P0-2 身份冒用 | 产品语义取舍,方案 A/B/C/D 见上。建议先定方案再动手 |
| 第 3 批(加固) | P1-2 戳文件门控、P1-3 状态校验、P1-4 建号护栏 | 独立可分批,P1-2 有现成范式 |
| 第 4 批(收尾) | P2-1 转 LF、P2-2 计数策略、P2-3 索引补偿、P2-4 去双转义 | P2-2 涉及经济体系,需你定口径 |
| 第 5 批 | P3-1~P3-5 | 顺手改 |
| 文档侧 | P3-6 更新规范 §9.1 :691 的过时注释 | 避免下次审计误判 |
七、总体评价
架构判断:插件对核心约束的理解是到位的 —— 钩子名/参数核实准确、双写策略(索引 + 分桶真相源)正确、明确避开了"直写核心 users 表"这个 §11.8 红线(注释里还写明了取舍理由)、后台路由与权限声明规范。基础工程质量高于本机插件平均水平。
问题集中在两处:
- 收尾未完成 —— P0-3 的提示钩子写了一半(注释写了"只输出提示"、en/zh_tw 语言键备好了,但
zh.php没补、echo 没写);P0-1 的class_alias漏了;P2-1 的行尾没统一。像是功能验证通过就停下了。 - 安全模型未闭环 —— P0-2 是设计层面的:插件把"用户名"当成可自由指派的标签,但在这个系统里用户名同时是登录凭据与权限标识。这不是编码疏忽,是需求定义时就该裁决的问题。
一句话:功能可用、基础规范扎实,但不能按现状开放给非管理员用户组使用 —— 开之前必须先解决 P0-2。
审计方式:只读 + 实测取证。本报告未改动任何插件文件、未修改任何业务数据。