[审计]网盘互助(三方版 `plugins/netdisk_help`)插件审计报告

👑Lv.11 元老 🌞 骨干会员
2026-10-25 00:39:41

本文由 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. 合规项(已通过,逐条有据)

  1. 目录结构完整:plugin.json / Plugin.php / hook/(11) / views/(4) / assets/(2) / lang/(3) / data/。
  2. plugin.json 字段齐全:name/version/author/description/activated/category/icon/admin_url/hooks/permissions;

admin_url 以 /admin/ 开头;permissions = ["route:admin","system:settings"](写 Settings 的权限已声明,§陷阱 67);hooks 指向的文件全部真实存在。

  1. ★ 生命周期类名推导已规避:Plugin.php:838-840 有

class_alias(Plugin::class, 'Plugin\\netdisk_help\\Plugin', false) —— 目录名含下划线,无此别名会让 activate/deactivate/uninstall 静默跳过(§11.12)。

  1. 钩子文件全部无顶层符号(10/10,§9.0);钩子内零 $this(§11.24)。
  2. 三语 i18n:zh/en/zh_tw 各 79 键,键集合零差异 + 键序一致 + 占位符集合一致;

反向核对「模板/控制器引用键 ↔ 字典」:0 缺失、0 死键(4 个「缺失」是动态拼装前缀 status_ / claim_status_ / fail_kind_ / verb_,非缺陷)。

  1. 编码卫生:无 BOM、全 LF、合法 UTF-8(24 个受检文件)。
  2. 零内联(静态):views/ + hook/ 源码零 style=;无 unserialize/eval/assert/preg_replace-e。
  3. 独立库 + 事务规范(§十):
  • 独立库 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)。
  1. 写入防护:链接字段过协议白名单(normalizeLink() 只放行 http(s)://,剥离 \r\n\t 并限长 500);code/note/reason 均 cut() 限长;输出一律 htmlspecialchars(ENT_QUOTES,'UTF-8')。
  2. 写操作令牌:前台 7 个动作走 HMAC-SHA256 签名 Nonce(会话密钥 + 600s TTL + 动作/资源绑定,Plugin.php:416-453);后台写操作走独立 admin CSRF token(verifyAdminCsrf())→ 不可跨站点伪造。
  3. 异步收尾挂点正确:hook/route_after_dispatch.php 做超时应助释放 + 到期求助退回,60s 文件节流(data/.cron.lock),不阻塞首字节(§11.20)。
  4. 防刷/防滥用: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:6 class AdminController extends Controller(app\Core\Controller),不是 app\Controllers\Admin\BaseController;

ndhEnsureAdmin()(:186-199)只调 requireAdmin()(管理员判定),未调 requireAdminVerified()。

  • 实测(本报告的证据链):同一管理员会话、admin_verified_at 缺失时
  • 核心 /admin/plugins → 302 Location: /admin/verify
  • 插件 /admin/netdisk-help → 200(正常渲染后台管理页)
  • 影响:二次密码验证形同虚设 —— 仅凭一个被盗的会话 Cookie 即可直接改插件设置、清除全部惩戒记录、解除封禁。
  • 建议:extends \app\Controllers\Admin\BaseController(自动获得管理员校验 + 二次验证门禁),或显式调 $this->requireAdminVerified()。

🟠 P1-5 自造管理员判定口径不一致(越权隐患)

  • 证据:Plugin.php:182-206 currentUserIsAdmin() 接受 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">&#xf0c2;</i>(核心 TemplateCompiler::icon() 恒带内联,§陷阱 36/72)。

  • 建议:改字面量 <i class="fa mn-fs-16" aria-hidden="true">&#xf0c2;</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:38 return $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/
19CSS 变量 --mn-info 核心未定义插件 CSS 用 var(--mn-info, …);核心 10 个样式源 0 处定义换用 --mn-bg-tag + --mn-primary 组合(其余 10 个变量 10/10 有定义 ✅)
20长文本截断用 mb_substrPlugin::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 → 打开该页就是没有左侧栏、宽度与其它后台页不一致的"裸页"。

因此本次按你的要求两件一起补:

  1. 后台页套上核心外壳(左侧栏才真的出现);
  2. 在侧栏「扩展」分组下注册插件入口(高亮当前页)。

注:本仓既有插件(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.jsonhooks 增 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. 建议修复顺序(待你定夺)

优先级项一句话
P0B-1通知改 Notification::add() —— 现在一条都发不出去
P0B-2版块列表改 Category::allOrdered() —— 现在后台选不到版块,发帖嵌入功能整体不生效
P0B-3删掉 4 条指向不存在方法的后台路由 —— 命中即 500
P0B-4后台控制器改继承 Admin\BaseController —— 现在绕过二次验证
P1B-5 ~ B-11管理员判定统一 Auth::isAdmin() / 写操作改 POST / 删孤儿钩子 / 设置保存改 array_key_exists / 导航去内联 / 大厅补 JS
P2B-12 ~ B-20i18n 补后台文案 / 补 404 状态码 / 复用核心后台 UI / 低危项

⚠️ 以上均未动手(除 D 节已交付的左侧栏改动)。需要我按哪几项修,说一声即可。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 3 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录