vn_postname(马甲发帖)插件问题汇总
版本:v1.1.0(plugin.json 声明)
审计依据:docs/插件开发规范.md(2026-09-22 版)
审计方式:只读审计 + 实测取证,未改动任何文件
结论:功能可用、基础工程质量扎实,但收尾未完成 + 一处设计级安全缺陷,共 17 项。
一、必须先看:一条安全缺陷
身份冒用 —— 可用 @@管理员名 以管理员身份发帖
位置:Plugin.php:175-211(resolveUser())
问题:插件对任意已存在的用户名直接返回其 uid,没有任何「只能用自己的马甲」或「禁止冒用高权限账号」的约束。
实测:
@@flinthub -> uid=1 (管理员)
@@虚拟化大师 -> uid=2
@@浏览器内核工程师 -> uid=3
后果链:
管理员在后台勾选「普通会员」组
任一普通会员发帖输入 @@flinthub
帖子作者双写为管理员(topic_index.uid + topic.uid)
前台显示管理员发的帖,管理员 post_count 也被 +1
同一机制下,普通用户也能 @@ 另一个普通用户来栽赃。
为什么现在没爆:当前后台 allowed_groups=[],只有管理员能用 → 风险被默认配置临时兜住。但插件卖点正是「按用户组授权」,一旦勾选任何普通组,风险立刻暴露。
这不是编码疏忽,是需求定义时漏掉的一环:插件把「用户名」当作可自由指派的标签,但在本系统里,用户名同时是登录凭据与身份标识。
二、收尾未完成的项
P0-1 生命周期类名推导失效 → 卸载留孤儿表
实测:
Plugin\vn_postname\Plugin class_exists = false
Plugin\VnPostname\Plugin class_exists = true
核心用目录名 vn_postname 拼主类名,实际命名空间是 Plugin\VnPostname。下划线不是大小写差异,class_exists 恒 false → activate() / deactivate() / uninstall() 三方法静默跳过。
真实后果:uninstall() 不执行 → 卸载后 vnpn_config / vnpn_log 两张表残留。
修法:Plugin.php 末尾注册 class_alias。本机已有 6 个先例(mod_system / vn_block / quiz_duel / third_party_login / points_mall / bitiful_storage)。
P0-3 发帖提示钩子是空实现
hook/post_create_extra.php 全文 20 行,末尾无任何 echo,但文件头注释写着「只输出提示」。
实测:GET /post/new 钩子锚点存在,但马甲提示出现 0 次。用户完全不知道有 @@ 功能。
连带:en.php / zh_tw.php 已写好 plugin.vn_postname.form_hint 文案,只有 zh.php 没有 —— 提示没写完就搁置的痕迹。
另::10 的 if (($target_type ?? '') === 'blog') 是死代码(该钩子注入参数为「无」)。
P1-1 三语语言包键集合不一致
zh=22 en=23 zh_tw=23
唯一缺键:plugin.vn_postname.form_hint(中文简体缺失)
一旦修 P0-3,中文用户会在发帖页看到键名原文。两条必须一起修。
P2-1 全部 12 个文本文件为 CRLF 行尾
规范要求 UTF-8 无 BOM + LF。实测 12/13 文件为 CRLF(plugin.json 是唯一 LF)。BOM 全部干净。
修法:批量转 LF 后必须用 Python 复核(grep/od 在本环境不可靠)。注意 plugins/mod_system/ 是规范明文例外(保持 CRLF),本插件不在例外清单内。
P3-1 版本号不同步
plugin.json = 1.1.0,Plugin.php:7 / AdminController.php:7 头注释 = 1.0.0。
三、加固项
P1-2 init_after 每请求跑 DDL
hook/init_after.php 无条件执行两条 DDL。ensureSchema() 本身无缓存门控。
实测开销:0.319 ms/请求。真正的代价不是耗时,而是每个前台请求都对插件 SQLite 库执行一次 DDL(走 schema 写事务),并发下争抢写锁。
修法:加戳文件 + 库文件双判门控(参照 points_mall/hook/init_after.php)。必须多判库文件存在,否则「戳文件在、库被删」会永久跳过建表。
P1-3 isAllowed() 不校验用户状态
判定链只有 role === 'admin' 或 group_id 在授权列表,未检查 users.status。系统存在 id=5「封禁账户」组。
风险:若勾选「普通会员」(id=1),而某用户 status='banned' 但 group_id 仍为 1 → 封禁用户仍可用马甲发帖。
P1-4 自动建号零护栏
allow_create 开启时对任意 @@用户名 建号。缺失护栏:
无用户名黑名单(可建 admin / 系统 等易混淆名)
无频率限制
无数量上限
不写插件关联表(user_level 的信任等级不会为新马甲建行)
P2-2 发帖数双计、马甲积分不同步
核心已给操作者 post_count + 1,插件再给马甲 +1 → 一次发帖,全站总数虚高 1。
另:核心给操作者加分,插件未给马甲加积分 → 马甲 points=0,等级/积分体系对马甲失效。
P2-3 搜索索引条件性不同步
核心先索引(标题仍带 @@用户名),插件随后改写标题。而 queue_mode='sync' 时索引永不重建 → @@用户名 词条残留。
实测:当前部署 queue_mode=cron,异步队列兜住,索引干净。缺陷为条件性。
修法:插件在 Thread::update() 后显式补一次 Search::indexThread()(幂等)。
P2-4 后台视图双重转义
views/admin.php:102 已转义,:111 再转一次。回落分支下含 </& 的用户名会被双转义(A&B → A&B)。
四、P3 级(不阻塞功能)
| # |
问题 |
| P3-2 |
uninstall() 无 try/catch,单表删除失败即中断,另一表残留 |
| P3-3 |
stripMarker 留空段落(<p>@@张三</p> → <p></p>) |
| P3-4 |
__nav_active='vn-postname' 是死参数 |
| P3-5 |
handlePostCreated() 的 $threadId 形参未使用 |
| P3-6 |
规范文档过时:§9.1 :691 注「post_create_after 在事务内」,实际在事务外。建议更新规范 |
五、值得肯定的部分(实测通过)
钩子名与参数全部核实正确,与规范一致
后台路由挂 admin_route_register,route:admin 权限已声明
CSRF 生效(无/错 CSRF 均 403);未登录 302 → /login
零内联(<style> / <script> / on* 零命中)
php -l 12/12 通过;无残留文件
不直写核心用户/积分表,全部走 Model 封装(注释里还写明了取舍理由)
双写改写正确(实测 topic_index.uid = 马甲 uid)
头像补齐生效(seed_avatars/2026-09/285.svg)
strip_marker 生效(实测帖子零 @@ 残留)
独立库 + 数据目录防护(.htaccess + web.config 自动生成)
后台 CSS 硬编码色有正当理由(后台无 --mn-* 变量),未污染前台
六、总体评价
架构判断到位:对核心约束的理解准确——钩子名/参数核实无误、双写策略正确、明确避开「直写核心 users 表」红线、后台路由与权限规范。基础工程质量高于本机插件平均水平。
问题集中在两处:
收尾未完成 —— 提示钩子写了一半、class_alias 漏了、行尾没统一。像「功能验证通过就停下了」。
安全模型未闭环 —— 身份冒用是设计层面的,不是编码疏忽。
一句话:功能可用、基础规范扎实,但不能按现状开放给非管理员用户组使用——开之前必须先解决身份冒用问题。
审计方式:只读 + 实测取证,未改动任何插件文件、未修改任何业务数据。