本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:7月5日复制的资料/全面代码审查报告_20260703.md。
SoSite 全面 PHP 代码审查报告
审查日期:2026-07-03
审查范围:全部 181 个 PHP 文件(Core / Controllers / Helpers / Models / Views)
审查方法:逐文件阅读 + 全局搜索交叉验证
基于版本:V3.9.0(拟定)
目录
一、安全漏洞(按严重程度排序)
🔴 P0 — 严重安全风险
1.1 RememberMe Token 永不轮换
- 文件:
app/Helpers/RememberMe.php - 风险:自动登录成功后不刷新
remember_token,Token 永久有效。一旦 Cookie 被窃取,攻击者可永久登录。 - 代码(第 33-41 行):
```php
if (hash_equals($user['remember_token'], $hashedToken)) {
session_regenerate_id(true);
$_SESSION['user_id'] = (int)$user['id'];
// ❌ 没有重新生成 remember_token 并更新数据库 + Cookie
}
```
- 修复:登录成功后生成新 token 存入数据库,并通过
setcookie更新客户端 Cookie。
1.2 renderBadge() 图标/标题未转义
- 文件:
app/Helpers/Points.php—renderBadge()第 112-123 行 - 风险:
$cfg['icon']和$cfg['title']直接从数据库/默认配置输出到 HTML,未做htmlspecialchars转义。虽然当前数据源非用户输入,但一旦level_config表被注入恶意内容,会导致存储型 XSS。 - 代码:
```php
return '<span class="level-badge" ...>'
. $cfg['icon'] . ' Lv.' . $level . ' ' . ($cfg['title'] ?? '新手') . '</span>';
```
- 修复:对
$cfg['icon']和$cfg['title']做htmlspecialchars转义。
1.3 GET 请求携带 CSRF Token
- 文件:
app/Controllers/Admin/MaintenanceController.php(第 16, 26, 35, 54, 77 行) - 风险:所有维护操作(重建索引、清理缓存、清理附件)使用
$_GET['csrf']校验,CSRF Token 会出现在浏览器地址栏和 Referer 头中,极易泄露。 - 修复:改为 POST 提交;或使用双重提交 Cookie 模式。
🟠 P1 — 高危风险
1.4 500 错误信息泄露
- 文件:
index.php(第 162-166 行) - 风险:未捕获异常时直接输出
$e->getMessage()和$e->getFile() . ':' . $e->getLine(),生产环境下会泄露内部路径、数据库表结构等敏感信息。 - 代码:
```php
echo '<p>' . htmlspecialchars($e->getMessage()) . '</p>';
echo '<p style="font-size:12px;color:#999;">' . htmlspecialchars($e->getFile() . ':' . $e->getLine()) . '</p>';
```
- 修复:生产环境关闭详细错误输出,记录到 error_log 即可。
- 工作量:3 行
1.5 登录锁定基于 Session(已知未修复)
- 文件:
app/Controllers/AuthController.php(第 27, 78-80 行) - 风险:
login_lock_until和login_attempts存储在$_SESSION中,攻击者更换 Session ID 即可绕过锁定限制无限尝试密码。 - 备注:此问题在 07-01 的
安全审查备忘.md中已标注为 P2,至今未修复。 - 修复:将锁定状态基于 IP 或用户名存储到数据库/缓存。
1.6 注册频率限制在验证码之后
- 文件:
app/Controllers/AuthController.php(第 113 行) - 风险:
RateLimiter::hit('register', 3, 3600)在验证码校验通过后(第 114 行)才执行。攻击者可利用无效验证码无限试探而不触发频率限制。 - 修复:将
RateLimiter::hit()移到Captcha::verify()之前。
1.7 搜索权限过滤全量拉取
- 文件:
app/Controllers/SearchController.php(第 41 行) - 风险/性能:每次搜索调用
Search::search($keyword, 'all', 1, 500),不论实际需要多少结果都拉 500 条,然后在 PHP 层做array_filter权限过滤。数据量大时内存和响应时间都不理想。 - 修复:改为在 SQL 层做权限过滤(
WHERE t.category_id IN ($allowedIds))。
🟡 P2 — 重要缺陷
1.8 TagsController 未实现分页
- 文件:
app/Controllers/TagsController.php - 问题:
Tag::getThreads()支持 page/perPage 参数,但show()方法不传分页参数,也不处理$_GET['page']。若某标签下有 1000+ 帖子,一次查询全部加载。 - 修复:添加分页参数传递和 page 读取。
1.9 $filteredRawThreads 可能未定义
- 文件:
app/Controllers/SearchController.php(第 74 行) - 问题:当
$type === 'all'且$keyword === ''时(不进第 36 行 if 分支),$filteredRawThreads变量未定义,但第 74 行array_values($filteredRawThreads ?? [])因??存在不会报错,但$filteredRawThreads本身未初始化仍有代码异味。 - 修复:在 if 分支外初始化
$filteredRawThreads = [];
1.10 Blog 封面删除路径保护不严谨
- 文件:
app/Controllers/BlogController.php(第 235 行) - 问题:
```php
if ($blog['cover_image'] && file_exists(UPLOAD_PATH . basename($blog['cover_image'])))
```
虽然有 basename() 防护,但 file_exists 不做 realpath 校验。当前无法利用,但防御深度不够。
1.11 缩略图原图删除路径未校验
- 文件:
app/Helpers/Upload.php(第 99-101 行) - 问题:
@unlink($sourcePath)中$sourcePath来自函数参数。虽然当前调用链中它是安全的后缀处理路径,但若未来扩展传入用户可控路径,可直接删除文件。
二、逻辑缺陷 / Bug
🐛 2.1 双模板引擎并存(View.php + TemplateCompiler.php)
- 文件:
app/Core/View.php(309 行)vsapp/Core/TemplateCompiler.php(435 行) - 问题:两个文件均有完整的
section/yield/extend/display/e/timeAgo/icon/highlightSearchTerms/decodeContent/formatPostContent/asset等方法,约 250 行重复代码。当前实际渲染使用TemplateCompiler,View.php仅Router::dispatch()中的 404 处理有唯一引用。 - 建议:统一移除
View.php,404 处理改用TemplateCompiler。
🐛 2.2 Search::syncThreadFTS() 和 syncBlogFTS() 空方法
- 文件:
app/Helpers/Search.php(第 210-221 行) - 问题:两个方法声明存在、方法体为空(注释说 MySQL FULLTEXT 实时同步无需手动更新)。但6 处调用点仍在调用它们:
PostController::create()— 第 115 行ThreadController::edit()— 第 220 行BlogController::create()— 第 163 行BlogController::edit()— 第 212 行Admin/ThreadController::index()— 第 39 行(批量转移时循环调用)- 影响:每次调用产生一次空函数调用开销(微乎其微),但误导读为者在后续维护中以为"手动同步"已经完成。
- 建议:保留空方法作为文档,或添加一句清晰注释并在将来移除调用点。
🐛 2.3 Plugin::hook('init_after') 被调用两次
- 文件:
app/init.php(第 32 行)+index.php(第 156 行) - 问题:
init.php中插件系统初始化后已调用一次Plugin::hook('init_after'),index.php路由注册后又调用了一次。如果任何插件监听了init_after,会执行两次。 - 建议:删除
index.php中多余的调用(第 156 行)。
🐛 2.4 邮件发送结果未检查
- 文件:
app/Controllers/AuthController.php(第 170 行) - 问题:
Mailer::send($email, $subject, $body)返回['success' => true/false, ...],但调用方完全不检查返回值。若 SMTP 配置错误,用户注册成功但永远收不到验证邮件(无任何提示)。 - 建议:检查返回值,失败时提示用户"注册成功但邮件发送失败,请联系管理员"。
🐛 2.5 AuthController::register() 两个不同位置的 ensureEmailVerifyColumns()
- 文件:
app/Controllers/AuthController.php(第 107 行和第 155 行) - 问题:
register()方法开始时调了一次ensureEmailVerifyColumns(),后面发验证邮件前又调了一次。虽然不会造成功能问题(第二次调用时表已存在直接 return),但属于冗余代码。
三、代码冗余 / 死代码
🗑️ 3.1 Database::getDbType() 恒返回 'mysql'
- 文件:
app/Core/Database.php(第 46-49 行) - 说明:MySQL-only 后遗留方法,项目中已无任何调用点调用来做分支判断。
🗑️ 3.2 Database::query() 中的 SQLite 兼容替换
- 文件:
app/Core/Database.php(第 54-55 行) - 说明:
```php
$sql = str_replace('INSERT OR IGNORE INTO', 'INSERT IGNORE INTO', $sql);
$sql = str_replace('INSERT OR REPLACE INTO', 'REPLACE INTO', $sql);
```
代码中已无任何 SQLite 语法调用(之前已全面清理),这些替换不再执行任何实际工作。
🗑️ 3.3 后台批量移动逻辑重复
- 文件:
app/Controllers/Admin/ThreadController.php和app/Controllers/Admin/BlogController.php - 说明:
batch_move的代码几乎完全一样(拼$placeholders→UPDATE ... WHERE id IN (...)),可提取为公共方法。
🗑️ 3.4 多个 Controller 直接 Database::getInstance()
- 文件:
ThreadController.php(第 53, 211 行)、PostController.php(第 86 行)、GroupController.php(第 35, 64 行) - 说明:大部分操作已通过 Model 封装 SQL,但这些地方直接使用
\app\Core\Database::getInstance()手写 SQL。SQL一致性迁移计划.md已有迁移规划。
四、性能优化建议
⚡ 4.1 搜索权限过滤方式需优化
| 当前 | 建议 |
|---|---|
Search::search($keyword, 'all', 1, 500) 拉 500 条,PHP 层 array_filter 过滤 | 改为在 FTS/LIKE 查询中直接 WHERE category_id IN ($allowedIds) |
| 每次搜索固定拉 500 条 | 按当前 tab 类型的实际分页大小拉取 |
⚡ 4.2 runtimeIncr() 每次操作都写 DB
- 文件:
app/Helpers/Settings.php(第 206-209 行) - 说明:每次发帖/回复都执行一条
ON DUPLICATE KEY UPDATE写入settings表。虽然每次一条 SQL 开销不大,但在高并发(如秒杀式灌水)下CAST(value AS SIGNED)的更新可能产生行锁竞争。 - 备选方案:使用内存数组批量累积,每 N 秒或在请求结束时统一写 DB。
⚡ 4.3 ProfileController.getStats() 4 条独立 COUNT 查询
- 文件:
app/Models/User.php(第 85-97 行) - 说明:
getStats()里分别执行了 4 条SELECT COUNT(*)/SELECT SUM()查询,可合并为一条多子查询 SQL 或使用缓存。
五、架构与代码规范建议
🏗️ 5.1 统一模板引擎:移除 View.php
- 影响范围:
app/Core/View.php整个文件(约 310 行)→ 删除 - 替换:
Router.php第 111 行的$view = new View(); $view->display('errors/404');改为(new TemplateCompiler())->display('errors/404');
🏗️ 5.2 $_SESSION['role'] 不再写入但需确认无残留引用
- 说明:07-02 下半场会话总结中提到"登录/RememberMe 不再写 role/username 到 Session",但部分旧模板或 Controller 可能仍硬编码读取
$_SESSION['role']。 - 建议:全局搜索
$_SESSION\[.role.\]和$_SESSION\[.username.\]确认无残留。
🏗️ 5.3 Model::findWhere() 接口不够纯粹
- 文件:
app/Core/Model.php(第 48-53 行) - 说明:
```php
$where = $this->buildWhere($conditions);
```
findWhere 接受 $conditions 可以是字符串(完整 SQL 片段)也可以是以数组。buildWhere() 对字符串直接原样返回,导致调用者要么传完整 SQL + params(如 User::findByUsername()),要么传数组。接口不一致容易误用。
🏗️ 5.4 hook('init_after') 调用位置标准化
第 2.3 条已经提到重复调用。建议统一规则:
- 只保留
init.php第 32 行的一次调用 - 删除
index.php第 156 行的冗余调用
六、优先级总览
按严重程度
| 优先级 | 问题编号 | 简述 | 估算工量 |
|---|---|---|---|
| 🔴 P0 | 1.1 | RememberMe Token 不轮换 | 10 行 |
| 🔴 P0 | 1.2 | renderBadge() 图标/标题 XSS | 2 行 |
| 🔴 P0 | 1.3 | GET 请求 CSRF Token 泄露 | 5 行 |
| 🟠 P1 | 1.4 | 500 错误信息泄露 | 3 行 |
| 🟠 P1 | 1.5 | 登录锁定 Session 化(已知) | 15 行 |
| 🟠 P1 | 1.6 | 注册频率限制顺序错误 | 1 行 |
| 🟠 P1 | 1.7 | 搜索权限过滤全量拉取 | 15 行 |
| 🟡 P2 | 1.8 | TagsController 未分页 | 5 行 |
| 🟡 P2 | 1.9 | 变量未初始化 | 1 行 |
| 🟡 P2 | 2.1 | 双模板引擎 | 拆分明细 |
| 🟡 P2 | 2.2 | 空方法无用调用 | 2 行 |
| 🟢 P3 | 2.3 | hook init_after 重复调用 | 1 行 |
| 🟢 P3 | 2.4 | 邮件发送结果未检查 | 5 行 |
| 🟢 P3 | 3.x | 代码冗余清理 | 综合评估 |
| 🟢 P3 | 4.x | 性能优化 | 综合评估 |
按改动类型
| 类型 | 问题数 | 说明 |
|---|---|---|
| 安全修复 | 7 | P0×3 + P1×3 + P2×1 |
| Bug 修复 | 6 | P2×3 + P3×3 |
| 冗余清理 | 5 | 可在日常维护中顺手修 |
| 性能优化 | 3 | 非紧急性,数据量大时再优 |
| 架构规范 | 4 | 长期可维护性改进 |
七、与历史审查的交叉对比
以下为 07-01/02 多轮审查中已发现但当前仍未修复的问题:
| 问题 | 首次发现 | 原始优先级 | 当前状态 |
|---|---|---|---|
| 登录锁定基于 Session | 安全审查备忘.md 3.1 | P2 | ❌ 未修复 |
| fillable 白名单 | 07-02 下半场会话总结 | P2 | ❌ 未开始 |
| SQL 一致性迁移(Controller 中 $db-> 搬到 Helper) | SQL一致性迁移计划.md | 中优先级 | ⏳ 未实施 |
| 旧帖子批量补缩略图脚本 | 07-02 会话总结 | 低 | ❌ 未编写 |
| 标签云颜色按频率分级 | 项目档案.md | 低 | ❌ 未做 |
以下为 07-01/02 已确认修复的问题(本次审查验证通过):
| 问题 | 状态 |
|---|---|
| role/group_id 强绑定 | ✅ 已修复 |
| Post POST 权限二次校验 | ✅ 已修复 |
| 管理员自降级保护 | ✅ 已修复 |
| 游客回退版块级别 | ✅ 已修复 |
| 搜索/标签/个人中心权限过滤 | ✅ 已修复 |
$perm 白名单 | ✅ 已修复 |
| 删除用户组事务保护 | ✅ 已修复 |
| CSRF Token 轮换(视图层) | ✅ 已修复 |
八、文件改动量预估
| 问题 | 涉及文件 | 改动量 |
|---|---|---|
| 1.1 RememberMe 轮换 | RememberMe.php + AuthController.php | ~10 行 |
| 1.2 renderBadge XSS | Points.php | 2 行 |
| 1.3 GET CSRF | MaintenanceController.php | 5 行 |
| 1.4 500 错误泄露 | index.php | 3 行 |
| 1.5 登录锁定 Session | AuthController.php | ~15 行 |
| 1.6 频控顺序 | AuthController.php | 1 行 |
| 1.7 搜索全量拉取 | SearchController.php | ~15 行 |
| 1.8 Tags 分页 | TagsController.php | 5 行 |
| 1.9 变量初始化 | SearchController.php | 1 行 |
| 2.1 双模板引擎 | View.php(删) + Router.php | 移除 309 行,改 2 行 |
| 2.2 空方法 | Search.php | 2 行注释 |
| 2.3 hook 重复 | index.php | 1 行删 |
| 2.4 邮件检查 | AuthController.php | 5 行 |
| 合计(安全+逻辑修复) | ~12 个文件 | ~65 行改动 |
| 可选(冗余清理) | 若干 | ~350 行(主要是删 View.php) |
文档维护:本报告基于 2026-07-03 全量 PHP 代码审查生成。每修复一条问题后,可在对应条目后标注
✅ 已修复及日期。