本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:7月5日复制的资料/修复完成测试.md。
第一轮修复完成测试记录
修复日期:2026-07-01
覆盖范围:🔴 致命漏洞 6 项(7 个文件)
1.1 ZIP 解压 RCE — Admin/ThemeController.php
改动文件: app/Controllers/Admin/ThemeController.php
修复内容
- 解压前遍历 ZIP 内所有文件,对每个文件后缀做白名单校验
- 白名单范围:
css, js, png, jpg, jpeg, gif, svg, ico, json, html, htm, woff, woff2, ttf, eot - 目录已存在时改为先删除旧目录再覆盖(原为报错提示)
- 解压后二次扫描目录,清理非白名单文件
- 解压完成后刷新主题缓存
测试要点
- [ ] 上传含
.php文件的 ZIP → 应拒绝 - [ ] 上传含
../路径的 ZIP → 应拒绝 - [ ] 上传合法的主题 ZIP → 应成功可用
- [ ] 再次上传同名主题 → 应覆盖旧主题
1.2 Upload.php 黑名单改白名单
改动文件: app/Helpers/Upload.php
修复内容
- 移除
$dangerousExts黑名单(原为php, php3, php4, php5, php7, php8, phtml, phar, cgi, pl, asp, aspx, jsp, sh, exe) - 改为完全依赖
$allowedTypes白名单控制 .pht,.shtml, 大写.PHP,.PhP等黑名单未覆盖的后缀现在都会拒绝- MIME 校验逻辑不变
测试要点
- [ ] 上传
.jpg文件 → 成功 - [ ] 上传
.php文件 → 拒绝 - [ ] 上传
.pht文件 → 拒绝(黑名单时代可绕过) - [ ] 上传
.PHP文件 → 拒绝(黑名单时代可绕过) - [ ] 上传
.shtml文件 → 拒绝 - [ ] 头像上传 → 正常可用
1.3 Plugin.php 任意文件包含防护
改动文件: app/Helpers/Plugin.php
修复内容
hook()中include $hookFile前增加realpath()校验- 使用
strpos($realHook, $realPluginDir) === 0确保钩子文件路径在插件目录内 - 路径非法时记录 error_log 并跳过,不中断页面
测试要点
- [ ] 安装并启用任意插件 → 插件功能正常
- [ ] 禁用插件 → 功能不再生效
- [ ] 修改 plugin.json 的 hooks 路径为
../→ 被拦截(记录日志) - [ ] 现有插件(daily_checkin、friend_links 等)→ 钩子正常触发
1.4 SSRF — ApiController::fetchImage()
改动文件: app/Controllers/ApiController.php
修复内容
- 原代码只校验了 URL 的 host 名字面值(如
localhost、192.168.x.x) - 新增
gethostbynamel()DNS 解析,对解析后的真实 IP 做私有网段过滤 - 禁止网段:
10.x.x.x、172.16-31.x.x、192.168.x.x、127.x.x.x、0.x.x.x
测试要点
- [ ] 粘贴外网图片(如
https://example.com/image.jpg)→ 正常抓取 - [ ] 传入
http://127.0.0.1:3306/→ 拒绝 - [ ] 传入
http://192.168.1.1/→ 拒绝 - [ ] 传入 DNS 指向内网的域名 → 拒绝
1.5 User.php 递归死循环 + 事务保护
改动文件: app/Models/User.php
修复内容
deleteWithCascade()所有 DELETE 操作包裹在begin/commit/rollback事务中- 中途失败全盘回滚,防止产生孤儿数据
- 递归调用问题已在之前的 SQL 一致性迁移中通过直接
$db->query('DELETE FROM threads...')解决
测试要点
- [ ] 删除一个普通用户 → 帖子、回复、附件全部清除
- [ ] 删除一个管理员(且为唯一管理员)→ 检查保护逻辑
- [ ] 模拟数据库异常断开 → 数据应回滚,不产生孤儿数据
1.6 Search.php DDL
改动文件: app/Helpers/Search.php
修复内容
initFTS()只检测索引是否存在,不再自动执行ALTER TABLE ADD FULLTEXT INDEX- 建索引操作剥离到后台「维护」页面手动执行
- 缺少索引时降级为 LIKE 搜索
测试要点
- [ ] 已有 FULLTEXT 索引 → 搜索功能正常
- [ ] 没有 FULLTEXT 索引 → 自动降级为 LIKE,搜索结果仍有但不走全文索引
- [ ] 后台「维护 → 重建全文索引」→ 能正常重建
回滚方式
备份文件在网站根目录的 _backup_firstround/ 下,如需回滚:
cp _backup_firstround/ThemeController.php app/Controllers/Admin/
cp _backup_firstround/Upload.php app/Helpers/
cp _backup_firstround/Plugin.php app/Helpers/
cp _backup_firstround/ApiController.php app/Controllers/
cp _backup_firstround/User.php app/Models/
cp _backup_firstround/Search.php app/Helpers/确认一切正常后可以删除备份目录:
rm -rf _backup_firstround第二轮修复详情
2.1 + 2.6 BaseController 二次验证 + 鉴权修复
改动文件: app/Controllers/Admin/BaseController.php
修复内容
- 二次验证增加 30 分钟超时校验(
time() - $verifiedAt > 1800) - 改用
parse_url($uri, PHP_URL_PATH)精确匹配路由路径,替换原来的strpos()字符串匹配 - 排除路径改为精确的
/admin/verify和/admin/verify/(不含查询参数)
测试要点
- [ ] 登录后台 → 正常访问
- [ ] 超过 30 分钟不操作 → 跳转到
/admin/verify重新验证 - [ ] 输入正确密码 → 恢复后台访问权限
- [ ] 构造带查询参数的
/admin/verifyURL → 不应绕过鉴权
2.2 管理员自降级保护
改动文件: app/Controllers/Admin/UserController.php
修复内容
- 唯一管理员保护触发时加入
return中断执行,防止updateByAdmin()被调用
测试要点
- [ ] 修改唯一管理员角色为
user→ 被拦截,提示错误 - [ ] 修改非唯一管理员角色 → 正常执行
- [ ] 修改自己的普通用户信息 → 正常执行
2.3 vote.js CSRF Token(已存在)
检查 assets/js/vote.js:
vote()函数第 36 行:if (window.CSRF_TOKEN) formData.append('csrf', window.CSRF_TOKEN);toggleVote()函数第 117-119 行:同样已附带 CSRF Token
结论:无需改动
2.4 logout 退出登录(已存在)
检查 app/Controllers/AuthController.php 第 152 行:
RememberMe::clearCookies();结论:logout() 已调用 clearCookies(),无需改动
2.5 后台 POST 补充 CSRF 校验
改动文件:
app/Controllers/Admin/BlogController.php— 博客分类新增/编辑 POST 分支(第 26 行)app/Controllers/Admin/CategoryController.php— 版块新增/编辑 POST 分支(第 27 行)app/Controllers/Admin/ThemeController.php— 各方法已有 CSRF 校验,无需改动
测试要点
- [ ] 后台新增博客分类 → 正常
- [ ] 后台编辑博客分类 → 正常
- [ ] 后台新增版块 → 正常
- [ ] 后台编辑版块 → 正常
2.7 文件删除安全
改动文件: app/Controllers/BlogController.php
修复内容
- 删除博客时的
unlink(UPLOAD_PATH . $blog['cover_image'])改为unlink(UPLOAD_PATH . basename($blog['cover_image'])) - 防止
$blog['cover_image']被注入../导致任意文件删除 ProfileController的unlink已使用realpath()校验(无需改动)Admin/UserController的unlink使用 Upload 生成的纯文件名(无需改动)
测试要点
- [ ] 删除带封面的博客 → 封面文件被删除,帖子正常删除
- [ ] 上传新封面 → 旧封面被删除
2.8 数据库导入路径穿越
检查 app/Controllers/Admin/DatabaseController.php:
handleImport() 使用 $_FILES['backup_file']['tmp_name'](PHP 上传临时文件路径),非用户输入路径。同时校验了扩展名 .sql。
结论:攻击面有限,无需额外改动
2.9 editor.js quote DOM-XSS
改动文件: assets/js/editor.js
修复内容
quotePost()函数中增加escapeHtml()安全转义函数- 对
username和content进行& < > " '的 HTML 实体转义 - 防止恶意用户名(如
<img onerror=alert(1)>)在引用时触发 XSS - 同时修复了 Markdown 模式和纯文本 Fallback 模式下的引用安全性(使用
safeContent)
测试要点
- [ ] 点击"引用"回帖 → 正常插入引用块
- [ ] 用户名包含
<script>alert(1)</script>→ 引用后显示为文本,不执行脚本 - [ ] Markdown 模式下引用 → 正常显示
- [ ] 旧浏览器 Fallback 模式 → 正常显示
回滚方式
第二轮备份在 _backup_secondround/ 目录下,如需回滚:
cp _backup_secondround/BaseController.php app/Controllers/Admin/
cp _backup_secondround/UserController.php app/Controllers/Admin/
cp _backup_secondround/BlogController.php app/Controllers/Admin/
cp _backup_secondround/CategoryController.php app/Controllers/Admin/确认正常后可以删除备份。
第三轮修复详情
3.1 Setting.php(Model)未定义变量 $qc
改动文件: app/Models/Setting.php
修复内容
WHERE {$qc} = :key中的$qc未定义变量,改为WHERE \key\= :key- PHP 8+ 严格模式下不再触发 Fatal Error
测试要点
- [ ] 后台「系统设置」页面 → 修改任意设置并保存 → 成功
- [ ] 修改站点名称 → 前台显示新名称
3.2 Content.php Base64 盲猜误判
改动文件: app/Helpers/Content.php
修复内容
- 增加
strlen($data) % 4 === 0校验(Base64 合法长度约束) - 增加
$decoded !== $data二次校验,解码结果与原文相同时不返回解码值 - 防止正常英文/技术文章被误解码成乱码
测试要点
- [ ] 发一篇纯英文技术博客(含代码)→ 内容正常显示,不出现乱码
- [ ] 发一篇含 Emoji 的中文博客 → 内容正常
- [ ] 编辑器正常发帖 → 内容显示正确
3.3 Blog.php 无条件 Content::decode()
已由 3.2 Content::decode() 改进覆盖($decoded !== $data 校验阻止了二次解码),无需额外改动。
3.4 Vote.php IN 子句拼装错误
检查确认:$placeholders 是经 intval 强转的整数直接拼接,非 PDO 占位符。无绑定问题,安全,无需改动。
3.5 Settings.php 写缓存缺 LOCK_EX
改动文件: app/Helpers/Settings.php
修复内容
buildCache()中file_put_contents($tmp, $content)加上LOCK_EX- 防止高并发写入损坏缓存文件导致站点 500
测试要点
- [ ] 后台保存设置 → 正常
- [ ] 后台「维护 → 清除缓存」→ 正常
3.6 Points.php LIMIT 参数绑定问题
改动文件: app/Helpers/Points.php
修复内容
LIMIT :lim改为LIMIT ' . (int)$limit强转后直接拼接- 兼容
PDO::ATTR_EMULATE_PREPARES => false模式
测试要点
- [ ] 积分日志页面 → 加载正常
- [ ] 调用
Points::getLog()→ 正常返回数据
3.7 附件清理误伤正在编辑的稿件
改动文件: app/Helpers/Settings.php
修复内容
cleanupOrphanAttachments()中删除文件前增加$file->getMTime() > time() - 86400跳过- 24 小时内上传的临时文件不会被当成孤儿删除
测试要点
- [ ] 上传附件但未提交帖子 → 立即执行清理 → 附件不被删除
- [ ] 上传附件超过 24 小时且未关联 → 可被清理
- [ ] 已关联帖子的附件 → 不受影响
3.8 高并发积分条件竞争(可延后)
当前项目规模下风险极低,暂不处理。
第四轮修复详情
4.1 sanitizeOrderBy 去反引号
改动文件: app/Core/Model.php
修复内容
- 正则
/[^a-zA-Z0-9_.,\s\]/去掉反引号\`` - 防止列名通过反引号逃逸注入
测试要点
- [ ] 排序相关功能(帖列表排序等)→ 正常
4.2 Captcha 验证销毁 Session
检查确认 Captcha.php 第 38 行已有 unset($_SESSION['captcha_hash'], $_SESSION['captcha_time']);,无需改动。
4.5 Theme.php 正则 \r 残留
改动文件: app/Helpers/Theme.php
修复内容
preg_match正则从/:\\s(.+?)$/mi改为/:\s([^\r\n]+)/i- 防止元数据尾部残留
\r回车符导致比对和排版异常
测试要点
- [ ] 后台主题管理 → 主题名称/版本信息显示正常
- [ ] Windows 环境下上传的主题元数据 → 无尾部 \r 残留
4.7 Controller 开放重定向
改动文件: app/Core/Controller.php
修复内容
redirect($url)增加 URL 校验:非本站域名的 HTTP URL 强制转为首页/- 防止
$_GET['redirect_url']等用户输入被利用做钓鱼跳转
测试要点
- [ ] 登录后重定向 → 正常
- [ ] 手动传入外域 URL → 跳转到首页
4.9 Plugin 写入锁 LOCK_EX
改动文件: app/Helpers/Plugin.php
修复内容
- 所有
file_put_contents()加上LOCK_EX独占锁(共 5 处:激活、激活回滚、禁用、更新配置、缓存写入) - 防止高并发操作损坏 plugin.json 或缓存文件
测试要点
- [ ] 后台启用/禁用插件 → 正常
- [ ] 编辑插件配置 → 保存正常
完整回滚方式
# 第一轮备份
cp _backup_firstround/* app/Controllers/Admin/
cp _backup_firstround/Upload.php app/Helpers/
cp _backup_firstround/Plugin.php app/Helpers/
cp _backup_firstround/ApiController.php app/Controllers/
cp _backup_firstround/User.php app/Models/
cp _backup_firstround/Search.php app/Helpers/
# 第二轮备份
cp _backup_secondround/BaseController.php app/Controllers/Admin/
cp _backup_secondround/UserController.php app/Controllers/Admin/
cp _backup_secondround/BlogController.php app/Controllers/Admin/
cp _backup_secondround/CategoryController.php app/Controllers/Admin/确认一切正常后删除备份:
rm -rf _backup_firstround _backup_secondround