本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:暂存20260913/合并安全审计与修复清单.md。
FlintHub 合并安全审计与修复清单
交叉验证来源:
mimo静态审计报告.md(mimo-v2.5-pro)+GLM静态审计.md交叉验证时间:2026-09-09
交叉验证方式:逐条回读真实源码,比对「报告所述文件名/行号」与「代码现状」;对报告中已存在防御逻辑的条目标注为「误报」并附代码证据。
纪律:本任务只做合并与交叉验证,未修改任何代码。
P0 必须修复(交叉确认)
两份报告各自独立指向同一条根问题,且都给出「入口 → 汇」完整链路。这是唯一重合项,必须修复。
1. P0 — base_path 设置项零校验 → 全站 JS 持久 XSS + 开放重定向(mimo #4 ∩ GLM M-2 重合)
- 重合依据:
- mimo #4(P1):SettingsController.php 将
base_path从$_POST直写数据库,无格式校验。 - GLM M-2(MEDIUM):同
SettingsController.php:24-27为存储点,指标main.php:66。
- 代码核实 — 存储点(真实,无校验):SettingsController.php
```php
foreach ($keys as $key) { // $keys 含 site_name/base_path/site_lang
$settingModel->setValue($key, $_POST[$key] ?? '');
}
```
仅校验 CSRF(L22),未对 base_path 做任何格式/字符校验。
- 代码核实 — 传播点(真实):init.php 将库内
base_path直接define('BASE_PATH', $bp)。
- 代码核实 — JS 字符串汇点(真实,未转义):main.php
```php
window.BASE_PATH='<?php echo defined('BASE_PATH') ? BASE_PATH : ''; ?>';
```
该值原样写入单引号 JS 字符串,未走 $this->e() / json_encode()。当 base_path 为 x';alert(document.cookie)// 时 ' 直接逃逸字符串 → 每页持久 XSS。
- 代码核实 — 重定向汇点(真实):Controller.php
redirect()在 URL 已以/开头且非//时追加BASE_PATH并Location:,若base_path='//evil.com'则所有重定向跳外域。
- 触发前提:需要有效管理员会话 + CSRF(GLM 的提权路径:仅劫持会话、未过二次验证时,借任一同域注入点写坏
base_path→ 先取得持久 XSS → 挂钩/admin/verify表单窃取二次密码 → 完整管理员)。
- 修复方案:服务端对
base_path强制白名单^\/[A-Za-z0-9_\-\/]*$(禁//、反斜杠、引号),非法值拒绝/回退空串;输出侧main.php:66改用json_encode(BASE_PATH)包裹。
P1 建议修复(单报告命中、经核实确认真实可修复)
1. P1 / HIGH — 后台标签管理存储型 XSS(GLM H-1,mimo 未报)
- 位置:app/Views/admin/tags.php:39
- 代码核实(真实):
onclick内 JS 单引号字符串prompt(..., '<?php echo $this->e($tag['name']); ?>')。$tag['name']仅经$this->e()(htmlspecialchars)输出,但该值处于onclick="..."双引号属性内 → 浏览器先做实体解码,'还原为'再交给 JS 引擎 → 逃逸字符串。 - 源 → 汇:任意注册用户创建标签名(Tag.php saveForThread 仅
trim+长度 1~30,无字符白名单,原文入库)→tags表 → 管理员打开后台标签页点击「重命名」触发。 - 证据对照:同文件 tags.php:42 删除按钮采用
htmlspecialchars(json_encode(...))正确姿势,L39 是漏网之笔 —— 印证误用而非刻意为之。 - 触发前提:需管理员一次点击交互(非纯被动),脚本以管理员身份执行。
- 修复方案:改与 L42 同款 ——
htmlspecialchars(json_encode($tag['name']), ENT_QUOTES, 'UTF-8');另建议在Tag::saveForThread对标签名加字符白名单。
2. P1 / MEDIUM — 重复点赞重复发积分(积分/等级体系可刷)(GLM M-1,mimo 未报)
- 位置:app/Controllers/ApiController.php:81-87 + app/Helpers/Vote.php:172-186
- 代码核实(真实):
Vote::toggle用ON CONFLICT(...) DO UPDATE SET vote=:vote2(L180),重复点赞时 UPSERT 仍"成功"且恒返回user_vote=$vote(L186);ApiController.php:81仅判$vote==1 && user_vote==1即Points::award(L84),无首次状态迁移判定、无 (投票者,目标) 去重。 - 可利用性:双小号循环「点亮→取消→再点亮」每次给目标发分;限流 30 次/分只减速不去重,可刷积分/等级及等级关联权限。
- 修复方案:
Vote::toggle返回"是否由非赞态首次变为赞态"(对比 UPSERT 前后实际行为差),仅状态迁移时发分;并对(voter, target)维度发分去重。
3. P1 / MEDIUM — SMTP 加密集硬编码默认值随源码分发(GLM M-3,mimo 未报)
- 位置:config.php:32(GLM 报 L31,实测为 L32)
- 代码核实(真实):
define('MAIL_PASS_KEY', 'SoSite_AES_Key_2026!@#XyZ7890AbCdEf=')为固定默认值随源码分发。FlintHub 为分发型程序,未改密钥的部署共享同一密钥;任何能读到源码的途径(git 泄露/备份/未来任意文件读)即可结合Mailer::decryptSmtpPass还原 settings 表中的 SMTP 授权码明文。 - 修复方案:密钥移出代码库(环境变量或
protected/下 Web 不可达的独立密钥文件),安装时随机生成;发布升级公告引导存量站点轮换密钥并重加密存量 SMTP 密码。
4. P1 / LOW — lang 切换 PRG 回跳开放重定向(GLM L-3,mimo 未报)
- 位置:app/init.php:121-131
- 代码核实(真实):
?lang=白名单通过后(L123-124),回跳Location:直接取原始REQUEST_URI仅剔除 lang 参数(L127-130),未拒绝//开头。诱导访问https://站点//evil.com/?lang=zh-cn→ 回跳Location: //evil.com/→ 跳外域,可作钓鱼跳板。 - 修复方案:回跳前仅取
parse_url($uri, PHP_URL_PATH)并拒绝//前缀。
5. P1 / LOW — /api/upload 绕过 can_attach 版块上传策略(GLM L-2,mimo 未报)
- 位置:app/Controllers/ApiController.php:126-149
- 代码核实(真实):
upload()仅校验 POST + 登录 + CSRF(L131-136)+ 限流(L141),直接调Upload::file(L142),完全不校验Permission::canAttach。被设为各版块can_attach=0的用户(发帖/回帖/编辑路径均被拦截)仍可直传白名单文件并获外链,绕过按版块上传策略及磁盘占用滥用。 - 修复方案:纳入统一上传权限体系,上传前校验目标版块
can_attach。
6. P1 / LOW — /api/vote 不校验目标版块可见性(GLM L-1,mimo 未报)
- 位置:app/Helpers/Vote.php:163-170(经 ApiController.php:66-75 调用)
- 代码核实(真实):
toggle前仅校验目标存在于topic_index/reply_index(L166-168),未按目标category_id调Permission::canView,软删目标亦不排除。登录用户可对无权浏览版块内容投票;配合上述重复点赞可给隐藏内容刷积分/形成存在性预言机。 - 修复方案:toggle 前补
canView+status校验。
7. P1 / LOW — /admin/verify 二次验证无限速(GLM L-4,mimo 未报)
- 位置:app/Controllers/Admin/VerifyController.php:18-28
- 代码核实(真实):仅
Csrf::verifyOrDie(L19)+password_verify(L22),无 RateLimiter/锁定/PoW(对比登录有双层限速)。前提是攻击者已持有有效管理员会话 + 页面 CSRF(会话劫持场景),此时可高速爆破二次验证密码突破最后一道防线。 - 修复方案:复用 RateLimiter(如 5 次/15 分钟)并在连续失败后使会话失效。
已忽略(误报 / 理论建议)
| 原报告条目 | 忽略原因(含代码证据) |
|---|---|
mimo #1 SSRF IPv6 绕过 isBlockedIp/isBlockedHost | [误报/理论] isBlockedIp/isBlockedHost(ApiController.php:267-280)确实缺 IPv6 段,但实际入口带有效防护:gethostbynamel()(L194)仅返回 IPv4 记录。字面 [::1]、[fd00::]、IPv4-mapped ::ffff: 均无法经 gethostbynamel 解析出 IP → 返回 false/空 → 直接拒绝(L195-198/L203);纯 AAAA 记录域名同样得到空结果被拒。报告所述「AAAA 指向 fd00::1」场景在 gethostbynamel 下不可达。属加固缺口、非可证实利用。 |
| mimo #2 SSRF GET 端点无 CSRF | [理论] ApiController.php:166-176 仅判断登录态且无 CSRF,但其响应是 JSON 返回受害浏览器而非攻击者侧,主要价值是「借已登录用户触发 SSRF」——依赖 #1 的 IPv6 绕过成立才成链,而 #1 已被 gethostbynamel 实效拦截。单独不构成可利用闭环。 |
| mimo #3 ZIP 解压符号链接任意覆写 | [理论] DatabaseController.php extractZipToData:776-786 确未检 symlink 条目,但导入/恢复均为管理员本人经 POST+CSRF 触发(L816/L862);需管理员主动导入恶意压缩包,属社会工程/管理员自伤,边际影响不大于管理员已有的 SQLite 导入能力。 |
mimo #5 模板 {!! !!} 原样输出 | [理论] TemplateCompiler.php:145 的 cbRaw 确不转义,但报告自述"当前视图中未发现对用户输入使用 {!! !!} 的情况",纯未来/规范建议,无现存触发点。 |
| mimo #6 SQLite 导入仅校验魔术字节 | [理论] DatabaseController.php handleImport:887-895 确仅验文件头,但导入同样为管理员本人经 POST+CSRF 触发(L862),恶意库等于管理员主动种下,非外部可利用入口。 |
| mimo #7 后台恢复 GET 触发状态变更 | [理论] MaintenanceController.php:58-79 GET 分支确无 CSRF,但动作是重建统计计数(total_*)非破坏,且 $pendingFile 仅在管理员刚发起 runtime_rebuild 后短暂存在;报告自述主要担心是"错误范式"而非实际危害。 |
| mimo #8 safeReferer 允许 localhost | [理论/低危] ApiController.php:650-661 白名单含 localhost/127.0.0.1,但影响需同机/同源,仅上传成功回跳,无实际开放重定向闭环,属加固建议。 |
| mimo #9 fetchHopByHop 禁用 SSL 验证 | [理论/加固] ApiController.php:306-309 / 347-348 确 verify_peer=false,却是 IP-replacement 防 DNS Rebinding 的必要副作用(连 IP 时证书主机名不匹配);后续有 MIME 白名单(L241-243)+ 大小上限兜底。Imagick 侧也已 setOption('svg:auto-graphic','false')(Upload.php:163)。 |
| mimo #10 上传扩展名白名单可配 | [误报/理论] Upload.php:25-28 白名单读后台,但其后有硬编码 MIME 映射表强制约束(Upload.php:61-79),后台把 php 加入白名单也会因 php 不在 $allowedMimes 中被拒;报告自述"当前安全",仅未来若扩展映射表才需再防。 |
GLM 低置信:Controller::redirect 未拦 /\ 变体 | [理论] Controller.php:97 仅拒非 / 与 //,/\evil.com 未拦,但当前调用方均传服务端构造路径,无端到端可控入口(GLM 自估 0.72 未达标上报门槛)。 |
| GLM L-5 uploads 无 .htaccess/web.config 禁执行 | [理论/加固] config.php:22 上传目录在 Web 根且内无禁执行文件,但白名单+MIME 硬约束当前使 .php/.svg 无法落盘,不可直接利用;仅迁移到 Apache/IIS 或配置漂移时才有洞,属纵深建议。 |
| GLM L-6 RememberMe 失配审计可伪造 | [低危] RememberMe.php:38-41 确可用 remember_user_id=<受害者> + 假 token 无鉴权写入失配审计,但仅污染日志(IP 取 REMOTE_ADDR 无法冒名),非直接被利用漏洞。 |
| GLM L-7 User::findByUsername/findByEmail 返回全行 | [理论] User.php:19-27 确返回 *(含 password hash),但 GLM 自述"未证实透出到响应",属最小取数规范建议,无实际泄露链路。 |
GLM 存疑:BaseController 未 use Csrf 致批量转移 500 | [功能缺陷非安全] BaseController.php:56 调 Csrf::verifyOrDie 但文件头(L9-12)未 use app\Helpers\Csrf,命名空间下解析为不存在的类 → 帖子/博客批量转移会 500;fail-closed 安全不受影响,仅功能不可用,顺手修复。 |
| GLM 存疑:DatabaseController mapBackupToTarget 前缀校验 | [理论] strpos($realTarget,$realRoot) 未加尾分隔符可兄弟目录前缀命中,但利用需管理员+导入 CSRF+目标已存在+限 .sqlite,危害边际。 |
| GLM 存疑:ExternStorage read/delete 汇点无路径校验 | [理论] 当前全部 extern_path 写入点均为 write() 内部生成、无外部入口;仅未来新增导入功能才成任意文件读写,属预留治理。 |
GLM 存疑:search_settings.php:43 $rebuildNext 未转义 | [理论] 来源为服务端拼接(int + urlencode token)无用户可控链路(SettingsController.php:467)。 |
GLM 存疑:custom_css 过滤可绕过无汇 / Model:219 ORDER BY 盲注 / CSP unsafe-inline | [理论] 均无实际可达汇点或当前为配置偏弱,属加固备注。 |
分级总结
| 等级 | 数量 | 说明 |
|---|---|---|
| P0 必须修复 | 1 | base_path 零校验 → 全站持久 XSS + 开放重定向(mimo #4 ∩ GLM M-2 交叉确认) |
| P1 建议修复 | 7 | 标签 XSS(H-1)、重复点赞刷分(M-1)、SMTP 密钥(M-3)、lang 开放重定向(L-3)、上传绕过 can_attach(L-2)、投票不校验可见性(L-1)、二次验证无限速(L-4) |
| 忽略 | 21 | 误报/理论/加固(详见上表) |
说明:P0 仅
base_path一项为两模型精确指认同一根问题的交叉确认。GLM 偏重业务逻辑/转义姿势,mimo 偏重 SSRF/管理后台,二者整体交集有限,其余条目经代码核实后按严重度计入 P1 或判为理论/误报忽略。