[审计]邀请注册(invite)插件 — 彻查报告

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

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/待办事宜/FlintHub_invite插件_彻查报告.md。

邀请注册(invite)插件 — 彻查报告

日期:2026-10-05 | 范围:plugins/invite/(15 文件 / 492 行)

方式:只读(未改任何插件代码);对照核心 modern.css / Csrf / Plugin / Controller 逐条验证

当前状态:plugins_cache.json 里 activated: false(未启用,plugins/invite/data/ 亦不存在)


一、结论摘要

#严重度问题位置
P1🔴 高(用户点名)注册页注入的邀请码输入框未适配暗夜星辰 → 白底黑字assets/script.js:15 + assets/style.css:4
P2🟠 中高邀请码大小写不对称 → 手输小写码被判「无效」hook/auth_register_before.php:10
P3🟠 中高无 PRG → 生成后刷新页面会重复生成 + 重复扣积分InviteController.php:35-73
P4🟡 中注册后标记邀请码缺 AND used_by_user_id IS NULL → 并发可覆盖hook/auth_register_after.php:15
P5🟡 中:focus / 必填星号硬编码色,未走主题变量assets/style.css:5,7
P6🟢 低输入框圆角/内距与核心 .mn-input 不一致(6px/12px vs 8px/13px)assets/style.css:4
P7🟢 低<div> 被注入 <head> 内(HTML 非法)hook/layout_head_end.php:14
P8🟢 低邀请码列表 LIMIT 10 无分页InviteController.php:80
P9🟢 低版本号三处不一致(1.6.0 / 1.5.0 / 1.0.0)plugin.json / 说明.md / 各 PHP 头
P10⚪ 极低熵 32bit、usedIds 含 0、relatedId 传 0、服务端无上界、子串匹配多处


二、P1 暗夜星辰适配(用户点名的问题)— 根因与证据

2.1 现象

注册页里,用户名 / 邮箱 / 密码 / 验证码框都是深色底浅字(随主题),

唯独邀请码框是白底黑字,在暗夜星辰下非常突兀。

2.2 证据链(三处代码)

① 插件注入的 input 是裸 <input>,没有任何 class —— assets/script.js:15

wrapper.innerHTML = '' + ... + ''
    + '';
    //        ↑ 没有 class="mn-input"

② 插件 CSS 只补了边框,没补 background / color —— assets/style.css:4

.invite-field input { width: 100%; padding: 9px 12px; border: 1px solid var(--mn-border, #ddd); border-radius: 6px; font-size: 14px; box-sizing: border-box; }
/*                    ↑ 无 background   ↑ 无 color */

③ 核心对「裸 input」只设字体,暗色适配全靠 .mn-input 类

  • assets/css/modern.css:26:input, textarea, select, button { font-family: inherit; font-size: inherit; outline: none; } —— 仅字体
  • assets/css/modern.css:1105-1114:

```css

.mn-input { width:100%; padding:9px 13px; border:1px solid var(--mn-border);

border-radius:8px; background:var(--mn-bg-input); color:var(--mn-text); font-size:14px; }

```

④ 对照核心注册页 —— app/Views/auth/register.php 里每个输入框都带 mn-input:

username / email / password / confirm_password / captcha 全部 class="mn-input"。

2.3 量化对照(探针实测)

=== 关键属性对照 ===
  background     | 核心 .mn-input: var(--mn-bg-input)  | 插件 .invite-field input: (无)
  color          | 核心 .mn-input: var(--mn-text)      | 插件 .invite-field input: (无)
  border         | 1px solid var(--mn-border)          | 1px solid var(--mn-border, #ddd)
  border-radius  | 8px                                 | 6px
  padding        | 9px 13px                            | 9px 12px

=== 暗夜星辰关键变量(modern-night.css)===
  --mn-bg-input = #16203a   ← 暗夜下输入框应有的底色
  --mn-text     = #e2e8f0
  --mn-border   = #2c3a4f

→ 因插件 input 无 class 且未声明 background/color,最终回落浏览器 UA 默认(白底黑字)。

CSS 变量切换主题不会影响 UA 默认值 → 暗夜下恒为白框。

2.4 修复方案(推荐 A)

方案改动说明
A(推荐)script.js:15 的 <input ...> 加 class="mn-input"一行,直接复用核心暗色适配;.invite-field input(0,1,1) 未声明 background/color,不会压掉 .mn-input(0,1,0) 的这两条
Bstyle.css:4 补 background: var(--mn-bg-input); color: var(--mn-text);不依赖 class,但两处样式源(核心/插件)易再分叉

建议 A + 顺手修 P5/P6(把插件 CSS 的圆角/内距对齐核心,#667eea→var(--mn-primary)、#e74c3c→var(--mn-error))。


三、P2 邀请码大小写不对称(功能 bug)

  • 生成(InviteController.php:42):$code = strtoupper(substr(bin2hex(random_bytes(4)), 0, 8)); → 存大写
  • 校验(hook/auth_register_before.php:10):$inviteCode = trim($_POST['invite_code'] ?? ''); → 原样查库
  • 输入框(script.js:15)也没有 text-transform: uppercase

→ 用户手输小写邀请码(如 a1b2c3d4)→ WHERE code = 'a1b2c3d4' 查不到 → 提示「邀请码无效或已过期」。

→ 修复:校验前 $inviteCode = strtoupper(trim($_POST['invite_code'] ?? ''));(一行)


四、P3 无 PRG → 刷新重复扣积分

  • InviteController::index() 在 POST 生成后直接渲染视图,不 redirect(:35-73 → :98)。
  • 核心 Csrf::verify() 不消费 Token(app/Helpers/Csrf.php:38-58 明确注释「校验本身不轮换 Token 值」,仅滑动续期 csrf_time)。
  • Csrf::token() 也只在 30 分钟过期时轮换(:17-24)。

→ 用户点「生成邀请码」后按 F5 / 浏览器回退再前进 → 重发同一 POST → CSRF 仍通过 → 再生成一个邀请码 + 再扣一次积分。

→ 修复:POST 成功后 redirect('/invites'),成功消息放 $_SESSION flash(符合核心「表单走 PRG」约定)。


五、P4 注册后标记缺并发保护

hook/auth_register_after.php:15:

$db->prepare('UPDATE invites SET used_by_user_id = :uid, used_at = :now WHERE id = :id')

  • 缺 AND used_by_user_id IS NULL。
  • 窗口:auth_register_before(校验通过)→ User::create() 等 → auth_register_after(标记)。同码被两人同时注册时,后者会覆盖前者的 used_by_user_id。

→ 修复:WHERE id = :id AND used_by_user_id IS NULL + 检查 rowCount()(=0 说明已被用,可记 error_log)。


六、已排除的怀疑项(彻查验证过、确认无问题)

项结论
i18n 键✅ 三语各 26 键,集合零差异;代码引用 26 键 ↔ 语言包零缺键、零冗余
视图类名✅ 41 个类名中 40 个核心存在;唯一 invite-code 是插件自定义类(在插件 style.css:9 定义)
getData('currentUser'/'csrfToken'/'siteName')✅ 核心 Controller::view()(:23-41)自动注入,视图取值有效
auth_register_after 的 $user_id✅ Plugin::hook() 用 `extract($params, EXTR_SKIP\
表单匹配✅ 表单 action = $this->url('/register') 与插件 data-action = BASE_PATH.'/register' 完全一致
Points::deduct 调用✅ 签名 (int $userId, int $points, string $reason, int $relatedId=0, string $relatedType='') 匹配
图标✅ icon('gift')、icon('users') 均存在(TemplateCompiler.php:697 等)
视图路径✅ view('plugins/invite/index') 由 resolveTemplate():67-74 解析到插件 views/
跨库事务✅ 插件库 + 核心库两连接无法共事务,代码已用「扣分失败即抛错回滚邀请码」正确处理(注释详实)
uninstall()✅ 只 DROP TABLE 符合核心「保留文件、插件自清数据」约定(Plugin.php:832)
XSS✅ script.js 的 innerHTML 数据来自 PHP 侧 htmlspecialchars(..., ENT_QUOTES) 转义后的 data-*
卸载残留✅ 无需删 data/(核心约定保留文件;且当前 data/ 不存在)


七、修复建议(按优先级)—— ✅ 已于 2026-10-05 全部实施,见 §九

第一批(必做,4 处 / 3 文件)

  1. assets/script.js:15 — input 加 class="mn-input"(修 P1)
  2. hook/auth_register_before.php:10 — 校验前 strtoupper()(修 P2)
  3. InviteController.php — POST 成功后 redirect + session flash(修 P3)
  4. hook/auth_register_after.php:15 — UPDATE 加 AND used_by_user_id IS NULL + 判 rowCount()(修 P4)

第二批(视觉统一,1 文件)

  1. assets/style.css — 圆角/内距对齐 .mn-input;#667eea→var(--mn-primary);#e74c3c→var(--mn-error)(修 P5/P6)
  2. hook/layout_head_end.php — <div id="inviteConfig"> + <script> 移到 layout_body_end,<head> 只留 <link>(修 P7)

第三批(打磨,可选)

  1. 列表分页 / LIMIT 放宽(P8);版本号统一(P9);relatedId 传真实邀请码 id(P10)

改完需按红线跑:php -l(8.2 + 8.0)、fh_i18n_check、php_var_cjk_check、plugin_hygiene;

并更新 plugin.json 版本 + 说明.md。


八、一句话给站长

邀请码框在暗夜星辰下是白的,根因是它没带 mn-input 类、插件 CSS 也没补 background/color(同表单其它框都带了)。

顺带查出 3 个真问题:手输小写邀请码会被判无效、生成后刷新会重复扣积分、注册标记缺并发保护。

→ 以上问题已于 2026-10-05 一次性全部修复,详见 §九。


九、修复实施记录(2026-10-05,站长批准「一次性修复完」)

9.1 改动清单(10 处 / 8 文件,含 1 新增)

#文件改动
1assets/script.js注入的 <input> 加 class="mn-input"、<label> 加 class="mn-label"、spellcheck="false"(P1)
2assets/style.css删冗余规则;.invite-field input 只留 width:100%(其余交核心);#e74c3c→var(--mn-error)(P5/P6)
3hook/auth_register_before.php邀请码 strtoupper 归一 + 非标量入参防护(P2)
4hook/auth_register_after.phpUPDATE 加 AND used_by_user_id IS NULL + rowCount() 检查(P4)
5InviteController.php抽出 handleGenerate(),统一 302 收尾(PRG);分页 LIMIT/OFFSET + 总数;relatedId 传真实邀请码 id;expires_days 服务端上界 365(P3/P8/P10/P13)
6views/index.php接 $this->pagination()(P8)
7hook/layout_head_end.php只留 <link>(P7)
8hook/layout_body_end.php(新增)#inviteConfig + <script>(P7)
9plugin.json注册 layout_body_end;版本 1.6.0 → 1.6.1(P7/P9)
10说明.md + 6 个 PHP 文件头版本号统一 1.6.1(P9)

9.2 验证结果(全绿)

项结果
php -l 8.210/10 通过
php -l 8.010/10 通过
php_var_cjk_check13 个 PHP 文件,无问题
i18n 键26 引用 = 26 定义,零缺键零冗余;三语键集合一致
逻辑探针 .workbuddy-ai/tmp/fh_inv_fix_verify.php18 PASS / 0 FAIL —— T1 大小写归一(含「修复前查不到」反向复现)/T2 并发保护(含「不带 IS NULL 会被覆盖」反向对照)/T3 PRG(真实调用 + 静态出口断言)/T4 分页边界(25 条 → 3 页 + 越界钳制)
静态断言10/10 修复点落盘(含 head 钩子无 <div> 输出、body 钩子含 #inviteConfig)
视图零内联 / lang 重复键通过
零残留探针改用 SQLite 内存库(无文件);未碰生产数据/配置/缓存

插件缓存:plugin.json mtime 已新于 plugins_cache.json → 按 Plugin.php:491 规则自动失效重建,无需手动清缓存。

9.3 边界说明

  • 未验证(需真实环境):暗夜星辰下的视觉效果(UI,交站长验收);启用后的真实注册全流程(需启用插件 + 邮件/积分环境)。
  • 未做(极低危,刻意未动):邀请码熵 32bit(UNIQUE 已兜底)、strpos 子串匹配 /register、nav_plugin_links 的 BASE_PATH 未加 defined 保护。
  • 回归可重跑:php .workbuddy-ai/tmp/fh_inv_fix_verify.php(幂等、零写入)。

十、发布记录(2026-10-05)

站长定版:1.7.0(minor —— 本次含新功能「邀请记录分页」)。§九 中所述 1.6.1 已按此要求上调为 1.7.0(3 处:plugin.json / 说明.md / 9 个 PHP 文件头)。

发布路径:Hub 官方(cli/release_build.php --type=plugin),非前台投稿包。

产物位置sha256
包体data/market/packages/flinthub-plugin-invite-1.7.0.zipf293978c22ac796914c6e0b3ee28f9f540b9ebe19831828bba4e5553d3c01815
清单data/market/manifests/manifest-invite-1.7.0.json1bee16e0dc5516e7fff95f5818dc4abda4bd0fe9154a3d763545208fb03eb3e8
镜像data/market/mirrors/flinthub-plugin-invite-1.7.0/18 文件

  • zip:20,051 B / 18 文件 + 6 目录,CRC 全通过;签名 RS256 / key_id=flinthub-2026。
  • 公开索引 release/plugins/index.json 已用 --type=index 重建 → invite 条目 1.7.0、signed: true。
  • 包内 plugin.json.activated = false(符合 §0.5b ① 新红线:启用与否由站长安装时决定)。
  • 旧版 1.6.0 保留(包/manifest/镜像均在;索引只展示最新版)。
  • 发布后校验 .workbuddy-ai/tmp/fh_plugin_release_verify.php invite → 30 PASS / 0 FAIL(通用脚本,可传任意插件名复用)。
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 3 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录