本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/docs/netdisk_help插件审计报告_2026-09-29.md。
网盘互助(三方版 plugins/netdisk_help)插件审计报告
审计日期:2026-09-29 | 审计对象:
D:\phpstudy_pro\WWW\plugins\netdisk_help(三方版,作者「读到」,v1.0.1)对照基准:
docs/插件开发规范.md(含 §9 钩子全表 / §11 常见坑 / §十二 开发流程与验证)审计方式:静态全量阅读 + 门禁脚本 + 真实 HTTP 端到端(
php -S+ 自造管理员会话)说明:「官方版」= 我们自己做的(
D:\phpstudy_pro\WWW\netdisk_help官方版,已发布,本报告不涉及)。
0. 结论速览
| 维度 | 结论 |
|---|---|
| 结构 / 清单 / 编码卫生 / i18n / 生命周期 | ✅ 合规(含目录名含下划线的 class_alias 已补) |
| 安全(事务、幂等、CSRF、路径与协议白名单) | 🟠 基本达标,但有 4 处口径偏差(见 B-4/5/6) |
| 功能可用性 | 🔴 3 处「功能静默失效」级缺陷(通知全失效 / 后台选不到版块 / 4 条后台路由 500) |
| 后台页 | 🔴 无核心后台外壳(已修)+ 🟠 绕过二次验证门禁 |
| 零内联 / 软 404 / i18n 覆盖 | 🟠 3 处偏差 |
门禁实测:plugin_hygiene.py → FAIL = 0;php -l 8.2 + 8.0 → 0 错;CJK 变量名 → 0 命中。
A. 合规项(已通过,逐条有据)
- 目录结构完整:
plugin.json/Plugin.php/hook/(11) /views/(4) /assets/(2) /lang/(3) /data/。 plugin.json字段齐全:name/version/author/description/activated/category/icon/admin_url/hooks/permissions;
admin_url 以 /admin/ 开头;permissions = ["route:admin","system:settings"](写 Settings 的权限已声明,§陷阱 67);hooks 指向的文件全部真实存在。
- ★ 生命周期类名推导已规避:
Plugin.php:838-840有
class_alias(Plugin::class, 'Plugin\\netdisk_help\\Plugin', false) —— 目录名含下划线,无此别名会让 activate/deactivate/uninstall 静默跳过(§11.12)。
- 钩子文件全部无顶层符号(10/10,§9.0);钩子内零
$this(§11.24)。 - 三语 i18n:
zh/en/zh_tw各 79 键,键集合零差异 + 键序一致 + 占位符集合一致;
反向核对「模板/控制器引用键 ↔ 字典」:0 缺失、0 死键(4 个「缺失」是动态拼装前缀 status_ / claim_status_ / fail_kind_ / verb_,非缺陷)。
- 编码卫生:无 BOM、全 LF、合法 UTF-8(24 个受检文件)。
- 零内联(静态):
views/+hook/源码零style=;无unserialize/eval/assert/preg_replace-e。 - 独立库 + 事务规范(§十):
- 独立库
plugins/netdisk_help/data/netdisk_help.sqlite,Plugin::db()自愈建表 +hook/init_after.php兜底(§11.15); - 写路径
claim/submit/confirm/reject/abort/cancel/releaseExpired/expireRequest/purge全部beginTransaction+ 条件 UPDATE +rowCount()判定 + 失败rollBack→ 状态字段即幂等锁,符合「先抢状态 → 再发钱」范式(§陷阱 104/118); Points::deduct()在前、业务写入在后,失败置failed并通知(§陷阱 103)。
- 写入防护:链接字段过协议白名单(
normalizeLink()只放行http(s)://,剥离\r\n\t并限长 500);code/note/reason均cut()限长;输出一律htmlspecialchars(ENT_QUOTES,'UTF-8')。 - 写操作令牌:前台 7 个动作走 HMAC-SHA256 签名 Nonce(会话密钥 + 600s TTL + 动作/资源绑定,
Plugin.php:416-453);后台写操作走独立 admin CSRF token(verifyAdminCsrf())→ 不可跨站点伪造。 - 异步收尾挂点正确:
hook/route_after_dispatch.php做超时应助释放 + 到期求助退回,60s 文件节流(data/.cron.lock),不阻塞首字节(§11.20)。 - 防刷/防滥用:
claimBlockReason()三重闸(同时应助上限 / 每日上限 / 频率窗口)+ 超时失败、投诉成立的递进惩戒 → 永久取消资格。
B. 缺陷清单(按严重度,含证据与建议)
🔴 P0-1 站内通知全部静默失效(功能级)
- 证据:
Plugin.php:688-700
```php
if (function_exists('create_notification')) { ...; return; } // ← 全仓不存在该函数
if (class_exists('\app\Helpers\Notification') && method_exists(...,'send')) { ... } // ← 核心没有 send()
```
核心实际入口是 app\Helpers\Notification::add(int $userId, string $type, string $title, string $link = '', ...)(app/Helpers/Notification.php:69)。
- 影响:应助提醒、提交提醒、确认/退回提醒、超时失败提醒、封禁/永久封禁通知 —— 一条都发不出去,且不写
error.log(两分支都不成立,静默)。 - 建议:改调
Notification::add($userId, 'netdisk_help', $message, $link, ...);并补一条「通知真的落库」的断言(notifications表行数增量)。
🔴 P0-2 后台选不到版块 → 插件核心玩法(发帖嵌入悬赏)整体不生效
- 证据:
AdminController.php:223-289的「兼容层」逐个试探方法名
all / getAll / getAllFlat / list / getList / getTree / tree / fetchAll(静态版同批),
而核心 app/Models/Category.php 的公开方法是 allOrdered()(Category.php:50)→ 一个都命中不了;
随后试探 \app\Models\Forum(该文件不存在)与 forums_cache() / get_forums() / ...(全仓均不存在,已 grep 验证)。
- 结果:
$rows恒空 → 视图走 fallback(views/admin.php:122-125显示「未能自动读取版块列表,请手动输入版块 ID」)→ 若管理员没手填,forum_id = 0
→ hook/post_create_extra.php:10 与 hook/thread_create_after.php:11 直接 return → 发帖页不出现悬赏输入框、发帖后不创建求助。
- 建议:改为
(new \app\Models\Category())->allOrdered()(或核心既有后台视图用的同一取法),并保留手填兜底。
🔴 P0-3 后台 4 条「兼容旧路径」路由指向不存在的方法 → 命中即 500
- 证据:
hook/admin_route_register.php:31-34注册
saveSettings / reportHandle / banLift / penaltyClear,
而 AdminController.php 中实际方法名是私有的 ndhSaveSettings / ndhReportHandle / ndhBanLift / ndhPenaltyClear(AdminController.php:70/130/159/173)。
- 影响:任何直接 POST 到
/admin/netdisk-help/settings(或 report-handle / ban-lift / penalty-clear)→Call to undefined method。 - 建议:删掉这 4 条(单入口
index()已按action分发),或把方法改名对齐。
🔴 P0-4 后台页绕过核心二次验证门禁
- 证据:
AdminController.php:6class AdminController extends Controller(app\Core\Controller),不是app\Controllers\Admin\BaseController;
ndhEnsureAdmin()(:186-199)只调 requireAdmin()(管理员判定),未调 requireAdminVerified()。
- 实测(本报告的证据链):同一管理员会话、
admin_verified_at缺失时 - 核心
/admin/plugins→ 302Location: /admin/verify - 插件
/admin/netdisk-help→ 200(正常渲染后台管理页) - 影响:二次密码验证形同虚设 —— 仅凭一个被盗的会话 Cookie 即可直接改插件设置、清除全部惩戒记录、解除封禁。
- 建议:
extends \app\Controllers\Admin\BaseController(自动获得管理员校验 + 二次验证门禁),或显式调$this->requireAdminVerified()。
🟠 P1-5 自造管理员判定口径不一致(越权隐患)
- 证据:
Plugin.php:182-206currentUserIsAdmin()接受role ∈ {admin, administrator, root, super}、任意is_admin/isAdmin/admin字段,不校验group_id;
核心权威口径 Auth::isAdmin()(app/Helpers/Auth.php:22-27)= role === 'admin' 且 group_id === 4(AND,不是 OR)。
- 现状:后台页因先走核心
requireAdmin()未被利用;但hook/thread_redpacket_area.php:17与views/_panel.php的$isAdmin只走这个宽松判定 → 只要有人把某账号role改成administrator(手工改库/别处插件),即可看到他人的应助资源。 - 建议:删掉自造实现,直接
\app\Helpers\Auth::isAdmin()。
🟠 P1-6 前台 7 个写操作走 GET + URL 内 Nonce
- 证据:
FrontController.php:10-32($_REQUEST+ 白名单 +verifyNonce);views/_panel.php:25-27, 67, 72, 75, 85, 119, 133, 145(全部<a href>/<form method="get">)。 - 风险:令牌虽为 HMAC 签名(安全性尚可),但落在 URL 上会经 Referer 头 / 浏览器历史 / 服务器访问日志外泄;GET 语义亦可被预取与爬虫触发。规范红线 8 要求写操作带 CSRF Token(POST)。
- 建议:改为 POST +
Csrf::verify()(或保留 Nonce 但放进请求体)。注意:改动会同时影响_panel.php的 7 处表单与ndhBack()的 Referer 回跳。
🟠 P1-7 孤儿钩子文件 hook/thread_operation_after.php
- 证据:该文件存在于
hook/,但plugin.json未登记(缓存亦只有 9 个钩子)→ 永不执行,属死代码。 - 附带隐患:它用
$GLOBALS['currentUser']['id']、$GLOBALS['csrfToken'](核心无这两个全局,grep已验),与thread_redpacket_area.php走Plugin::currentUserId()的口径不一致;
若日后把它登记进 plugin.json,因 _panel.php 无「只渲染一次」防护,帖子详情页会出现两条互助状态条(thread_redpacket_area 已渲染一条)。
- 建议:删除该文件,或改为「面板只渲染一次」的哨兵(如
static $rendered)。
🟠 P1-8 后台设置「字段缺失即静默写默认值」
- 证据:
AdminController.php:73
$i = static fn(string $k, int $d, int $min, int $max): int => min($max, max($min, (int)($_POST[$k] ?? $d)));
- 风险:表单少一个字段(改名、裁剪、只提交部分 Tab)→ 该键被写成默认值而非跳过 → 管理员改过的配置被悄悄回退(§陷阱 75 同源)。
- 建议:数值键一律
if (!\array_key_exists($key, $_POST)) { continue; }(勾选框另算:缺失 = 显式关闭)。
🟠 P1-9 saveConfig() 里的 Settings::update() 是死调用
- 证据:
Plugin.php:269-275循环$cls::$m('netdisk_help', $cfg);核心签名Settings::update(string $key, string $value, string $pluginName='')→ 第二参传数组 →TypeError→ 被外层catch (\Throwable)静默吞掉。 - 影响:无实际危害(不会写脏数据),但说明该路径从未被验证过,误导后来人。
- 建议:删掉这段(配置已落在插件自己的
ndh_settings表)。
🟠 P1-10 运行期零内联违规(前台导航)
- 证据:
hook/nav_plugin_links.php:16用$view->icon('cloud', 16);HTTP 抓首页实测输出
<i class="fa" style="font-size:16px"></i>(核心 TemplateCompiler::icon() 恒带内联,§陷阱 36/72)。
- 建议:改字面量
<i class="fa mn-fs-16" aria-hidden="true"></i>(.fa的font-size:inherit已于 2026-09-24 移除,mn-fs-N直接生效)。
🟠 P1-11 大厅页不加载 assets/script.js → 倒计时空白
- 证据:
hook/layout_head_end.php:7只对template === 'thread/show'注入;views/hall.php:94却输出<span class="ndh-countdown" data-ndh-deadline="…"></span>。
HTTP 抓 /netdisk-help 验证:页面不含 netdisk_help/assets/script.js(只有核心脚本与其它插件的脚本)。
- 影响:大厅列表里「应助进行中」的倒计时永远空白(
data-ndh-code/data-ndh-toggle/data-confirm若在该页出现同样失效)。 - 建议:
views/hall.php与views/mine.php在section('js')里补脚本(与 CSS 同法),或把layout_head_end的模板判定扩到插件自己的页。
🟠 P1-12 后台页文案硬编码中文,未走 i18n
- 证据:
views/admin.php全文('基础设置'、'保存全部设置'、'投诉成立'…)+FrontController.php的 flash 文案('应助成功,请在 X 分钟内提交网盘资源'等)。 - 影响:英文 / 繁中站点后台仍显示中文(前台视图已全部走 i18n,仅后台与 flash 未走)。
- 建议:补
plugin.netdisk_help.admin_*键(三语同步,注意键序一致)。
🟠 P2-13 软 404(HTTP 200)
- 证据:
FrontController.php:38return $this->view('errors/404', [...]);未设状态码 → 插件关闭时大厅返回 HTTP 200(§陷阱 74)。 - 建议:渲染前
\http_response_code(404);(范式plugins/points_mall/FrontController.php:227)。
🟠 P2-14 未复用核心后台 UI 范式
- 后台 Tab 自造
.ndh-admin-tab(规范 §11.10 要求复用核心.admin-tabs/.admin-tab-btn/.admin-tab-pane);
未使用 .admin-section / .admin-table.admin-table-kv / .plugin-status-on|off(§陷阱 84)。
🟡 P3 低危 / 建议
| # | 问题 | 证据 | 建议 |
|---|---|---|---|
| 15 | 登录跳转硬编码 /login,无回跳 | FrontController.php:506-512 | 用 Auth::loginUrl() + 记住来源(§11.28) |
| 16 | 导航入口输出 class=""(空 class) | nav_plugin_links.php:14-19 的 ltrim($active) | 直接拼 sub-link 语义的类名 |
| 17 | 大厅关闭后 route_after_dispatch 仍在扫描 | hook/route_after_dispatch.php 不看 $cfg['enabled'] | 加开关判定(口径一致) |
| 18 | 运行期产物混在 data/ | data/.cron.lock、data/last_error.log | 打包发行版必须排除;或改放 data/runtime/ |
| 19 | CSS 变量 --mn-info 核心未定义 | 插件 CSS 用 var(--mn-info, …);核心 10 个样式源 0 处定义 | 换用 --mn-bg-tag + --mn-primary 组合(其余 10 个变量 10/10 有定义 ✅) |
| 20 | 长文本截断用 mb_substr | Plugin::cut() | 纯文本字段目前安全;若将来渲染 HTML 需改 Content::excerpt()(§132) |
C. 关于「后台左侧栏」与规范 §9.1 #13
规范 §9.1 #13(admin_sidebar_links)备注「新插件一律不注册,用 plugin.json 的 admin_url」——
其前提是「插件后台页自己会套核心后台外壳」,入口从 后台 → 插件 → 设置 进。
但三方版原先连外壳都没有:views/admin.php 直接 <div class="ndh-admin-wrap">,
既无 .admin-container、也无 admin/_sidebar → 打开该页就是没有左侧栏、宽度与其它后台页不一致的"裸页"。
因此本次按你的要求两件一起补:
- 后台页套上核心外壳(左侧栏才真的出现);
- 在侧栏「扩展」分组下注册插件入口(高亮当前页)。
注:本仓既有插件(nav_manager / announcements / friend_links)也是这么注册的,有先例。
D. 本次改动(4 文件 + 1 新建)
| 文件 | 改动 | sha256(前16) |
|---|---|---|
hook/admin_sidebar_links.php | 新建:.sub-link + 字面量图标 <i class="fa mn-fs-14"> + i18n 文案 + 自身路径高亮;无顶层符号 | d16facb7a1e0424f |
plugin.json | hooks 增 admin_sidebar_links(9 → 10) | 17822799137ea3b7 |
lang/zh.php lang/en.php lang/zh_tw.php | 各 +1 键 plugin.netdisk_help.admin_sidebar_link(插在 admin_title 之前,三语键序一致;78 → 79) | 880085d0561ce000 / c2947a0e255411c8 / e35d3ac921e93d8f |
views/admin.php | 套核心后台外壳:.admin-container + include('admin/_sidebar') + .admin-content + .admin-header(页头图标用 mn-fs-16 字面量,零内联) | 5a06f8569e6cbc52 |
E. 验证记录
静态门禁
php -l 8.2 / 8.0 → 21 个文件 0 错
plugin.json → JSON 合法
php_var_cjk_check → 0 命中(全角标点紧跟 $var)
plugin_hygiene.py → FAIL = 0(三语键集合/键序/占位符一致;BOM/CRLF 0;hooks 文件存在;零内联 PASS;缓存一致)HTTP 端到端冒烟 .workbuddy-ai/tmp/ndh_sidebar_smoke.sh → 16 PASS / 0 FAIL
| 断言 | 结果 |
|---|---|
| 插件后台页 200 且正文非空 | ✅ 200 / 31488 B |
.admin-sidebar / .admin-container / .admin-content / .admin-header | ✅ 4/4 出现 |
左侧栏入口 href="/admin/netdisk-help" class="sub-link active" | ✅ 存在且高亮 |
核心 /admin/plugins 侧栏也带该入口(非高亮) | ✅ |
插件自渲染区间(<main class="admin-content"> → </main>)内联 style= | ✅ 0 |
插件页头图标无内联(未用核心 icon()) | ✅ 0 |
| 游客访问插件后台页 | ✅ 302 |
前台大厅 /netdisk-help 未被影响 | ✅ 200 |
error.log 增量 | ✅ 0 |
测试会话按精确 SID 删净(ndhsb% 残留) | ✅ 0(未触碰站长真实会话与任何业务数据) |
审计证据(P0-4 的复现):同一管理员会话、缺 admin_verified_at 时
/admin/plugins → 302 /admin/verify(核心) vs /admin/netdisk-help → 200(插件)。
F. 建议修复顺序(待你定夺)
| 优先级 | 项 | 一句话 |
|---|---|---|
| P0 | B-1 | 通知改 Notification::add() —— 现在一条都发不出去 |
| P0 | B-2 | 版块列表改 Category::allOrdered() —— 现在后台选不到版块,发帖嵌入功能整体不生效 |
| P0 | B-3 | 删掉 4 条指向不存在方法的后台路由 —— 命中即 500 |
| P0 | B-4 | 后台控制器改继承 Admin\BaseController —— 现在绕过二次验证 |
| P1 | B-5 ~ B-11 | 管理员判定统一 Auth::isAdmin() / 写操作改 POST / 删孤儿钩子 / 设置保存改 array_key_exists / 导航去内联 / 大厅补 JS |
| P2 | B-12 ~ B-20 | i18n 补后台文案 / 补 404 状态码 / 复用核心后台 UI / 低危项 |
⚠️ 以上均未动手(除 D 节已交付的左侧栏改动)。需要我按哪几项修,说一声即可。