[审计]SoSite 全面 PHP 代码审查报告

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

本文由 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 行)vs app/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 行的冗余调用

六、优先级总览

按严重程度

优先级问题编号简述估算工量
🔴 P01.1RememberMe Token 不轮换10 行
🔴 P01.2renderBadge() 图标/标题 XSS2 行
🔴 P01.3GET 请求 CSRF Token 泄露5 行
🟠 P11.4500 错误信息泄露3 行
🟠 P11.5登录锁定 Session 化(已知)15 行
🟠 P11.6注册频率限制顺序错误1 行
🟠 P11.7搜索权限过滤全量拉取15 行
🟡 P21.8TagsController 未分页5 行
🟡 P21.9变量未初始化1 行
🟡 P22.1双模板引擎拆分明细
🟡 P22.2空方法无用调用2 行
🟢 P32.3hook init_after 重复调用1 行
🟢 P32.4邮件发送结果未检查5 行
🟢 P33.x代码冗余清理综合评估
🟢 P34.x性能优化综合评估

按改动类型

类型问题数说明
安全修复7P0×3 + P1×3 + P2×1
Bug 修复6P2×3 + P3×3
冗余清理5可在日常维护中顺手修
性能优化3非紧急性,数据量大时再优
架构规范4长期可维护性改进


七、与历史审查的交叉对比

以下为 07-01/02 多轮审查中已发现但当前仍未修复的问题:

问题首次发现原始优先级当前状态
登录锁定基于 Session安全审查备忘.md 3.1P2❌ 未修复
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 XSSPoints.php2 行
1.3 GET CSRFMaintenanceController.php5 行
1.4 500 错误泄露index.php3 行
1.5 登录锁定 SessionAuthController.php~15 行
1.6 频控顺序AuthController.php1 行
1.7 搜索全量拉取SearchController.php~15 行
1.8 Tags 分页TagsController.php5 行
1.9 变量初始化SearchController.php1 行
2.1 双模板引擎View.php(删) + Router.php移除 309 行,改 2 行
2.2 空方法Search.php2 行注释
2.3 hook 重复index.php1 行删
2.4 邮件检查AuthController.php5 行
合计(安全+逻辑修复)~12 个文件~65 行改动
可选(冗余清理)若干~350 行(主要是删 View.php)


文档维护:本报告基于 2026-07-03 全量 PHP 代码审查生成。每修复一条问题后,可在对应条目后标注 ✅ 已修复 及日期。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 瀏覽 4 次 | 回覆 0 次

全部回覆 (0)

暫無回覆

請 登入