本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:7月5日复制的资料/代码审查报告.md。
SoSite 代码审查报告
审查日期:2026-07-01
审查范围:PHP 后端核心代码(Core、Helpers、Models、Controllers、Config 等)
审查目标:安全性、逻辑正确性、冗余代码
目录
1. 严重 Bug(可能导致崩溃或数据损坏)
1.1 Model.php — insert() 方法引用了未定义的属性 $this->dbType ⚠️
文件: app/Core/Model.php 第 83-87 行
代码:
if ($this->dbType === 'mysql') {
return $this->db->getConnection()->lastInsertId();
} else {
return $this->db->getConnection()->lastInsertRowID(); // SQLite-only
}问题: 属性 $this->dbType 从未定义/初始化,且该分支在 MySQL 环境下走 else 分支调用 lastInsertRowID()(此为 SQLite 方法),MySQL 无此方法,将抛出 Call to undefined method 致命错误。
影响: 任何 Model::insert() 调用都会引发致命错误,导致发帖、注册等核心功能全部崩溃。
修复: 移除该兼容性分支,直接使用 return $this->db->lastInsertId();
1.2 Captcha.php — 使用 PostgreSQL/SQLite 语法 ON CONFLICT ❌
文件: app/Helpers/Captcha.php 第 18 行
代码:
$db->query("INSERT INTO settings (key, value) VALUES ('captcha_salt', :v) ON CONFLICT(key) DO UPDATE SET value = :v2",
[':v' => $salt, ':v2' => $salt]);问题: ON CONFLICT ... DO UPDATE 是 PostgreSQL 和 SQLite 3.24+ 的语法。MySQL 使用 ON DUPLICATE KEY UPDATE。由于项目仅支持 MySQL,此语句执行会抛出 SQL 语法错误,导致验证码功能首次使用时崩溃。
影响: 验证码功能首次调用 getSalt() 时会失败。
1.3 Settings.php — updateOnlineStatus() 仍保留 SQLite 分支
文件: app/Helpers/Settings.php 第 311-319 行
代码:
if ($dbType === 'mysql') {
$db->query('REPLACE INTO ...');
} else {
$db->query('INSERT OR REPLACE INTO ...');
}问题: 由于 Database::query() 会自动将 INSERT OR REPLACE INTO 替换为 REPLACE INTO,该分支完全多余。且 getDbType() 始终返回 'mysql',else 分支永远不会执行。
影响: 死代码,但无运行时风险。
1.4 ThreadController.php — 直接使用 INSERT OR IGNORE 但依赖 Database::query() 的字符串替换
文件: app/Controllers/ThreadController.php 第 43 行
代码:
$db->query("INSERT OR IGNORE INTO viewed_replies ...");问题: Database::query() 的字符串替换 str_replace('INSERT OR IGNORE INTO', 'INSERT IGNORE INTO', $sql) 能正确处理。但如果未来更换数据库驱动或绕过 query() 方法直接使用 PDO,会立刻报错。
影响: 当前运行正常,但有脆弱性。
2. 安全漏洞
2.1 BaseController — 非管理员可访问 /admin/verify 页面
文件: app/Controllers/Admin/BaseController.php 第 8-18 行
代码:
public function __construct()
{
if (!Auth::isAdmin()) {
\header('Location: /login');
exit;
}
$uri = $_SERVER['REQUEST_URI'] ?? '';
if (strpos($uri, '/admin/verify') === false) {
Auth::requireAdmin();
}
}问题: 当非管理员访问 /admin/verify 时,BaseController 检查 isAdmin() → false,跳转到 /login。但 验证完成后 直接设置 admin_verified_at 并重定向到 /admin/,随后 BaseController 再次判断 isAdmin()(此时 role 已是 admin,因为已在 session 中)——所以实际运行时没问题。
但潜在风险:如果 session 中 role 不是 admin 但 user_id 存在,访问 /admin/verify 后 VerifyController 的 password_verify 使用登录用户的密码,非管理员用户可以用自己的密码验证自己的身份,但验证的是"已登录用户"而非"管理员用户"。所以非 admin 用户的密码验证通过后,admin_verified_at 被设置,但下次 requireAdmin() 检查时 isAdmin() 返回 false(因为 role 不是 admin),所以仍然会跳转。实际上安全,但逻辑令人困惑,建议明确检查 role。
2.2 AJAX 投票 API 缺少内容所有者的双重验证
文件: app/Controllers/ApiController.php 第 67-75 行
问题: API 投票会给内容作者增加积分奖励,但只通过 Csrf::verify() 验证一次。虽然 CSRF token 能阻止跨站请求,但如果攻击者能获取 CSRF token(如 XSS),可以批量操纵投票。建议在投票 API 中也校验 RateLimiter 已在第 31 行调用,但需要确认 RateLimiter 的 Session key 使用 IP 计算,同一个 NAT 后的多个用户共享同一 IP,可能导致误杀或绕过。
2.3 RateLimiter 的 IP 标识容易被绕过
文件: app/Helpers/RateLimiter.php 第 76-80 行
代码:
private static function buildKey(string $action): string
{
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
return '_rate_limit_' . $action . '_' . \crc32($ip);
}问题:
- 使用
crc32(哈希碰撞低)作为标识,容易碰撞 - 仅依赖
REMOTE_ADDR,反向代理后始终为 127.0.0.1 - 依赖 Session 实现限流,但 Session 可能被并发请求竞争
2.4 删除用户时未清理 remember_token
文件: app/Controllers/Admin/UserController.php 第 73-106 行
问题: 删除用户时虽然删除了用户记录,但已经下发的 remember_token cookie 仍然在用户浏览器中。如果之前有 token 泄露,攻击者仍可以在用户被删除前尝试使用。建议删除用户时明确清除其 remember_token。
3. 数据库 Schema 与实际代码不匹配
3.1 attachments 表缺少 user_id 字段
Schema (database/schema.sql): 无 user_id 列
引用代码 (app/Controllers/Admin/UserController.php 第 94 行):
$attachments = $db->fetchAll("SELECT filename FROM attachments WHERE user_id = :id4 ...");影响: 删除用户时尝试按 user_id 查询附件,但数据库中无此列,查询将静默返回 0 行,导致用户附件未被删除清理。
3.2 threads 表缺少多个关键字段
Schema 中 threads 表仅有 8 列,但 代码中大量使用以下字段:
| 字段名 | 引用位置 |
|---|---|
deleted_at | Thread::softDelete(), getDeleted(), countDeleted(), softDelete() |
last_reply_at | Thread::getLatest(), getLatestPaginated(), getByCategoryPaginated(), update() |
is_pinned | Thread::getLatest(), getLatestPaginated(), getByCategoryPaginated() |
is_highlighted | Admin/ThreadController |
color | Admin/ThreadController |
reply_to_view | ThreadController::show(), PostController::create() |
影响: 这些字段要么不存在,要么只在 install.php 中通过 database/init.php 动态创建。如果直接使用 schema.sql 建表,这些功能将因字段缺失而崩溃。
3.3 posts 表缺少 deleted_at 字段
Schema 中 posts 表同样没有 deleted_at 列,但 Model/Post.php 的 softDelete() 方法会执行 UPDATE posts SET deleted_at = :now WHERE id = :id——SQL 不会报错(因为字段不存在时 UPDATE 不会影响 rows_affected),但软删除永远不会生效。
3.4 users 表缺少多个字段
Schema 中缺少:last_active_at, remember_token, theme, points, level, status, avatar, signature
这些字段在代码中被多处引用,如果 install.php 的初始化未正确执行,会引发 SQL 错误。
4. SQLite 残留代码(本应已清理)
根据 项目档案.md,"已完成 SQLite 全面清理",但仍发现多处残留:
| 文件 | 行号 | 遗留代码 |
|---|---|---|
Search.php | 68-76 | initFTS() 中的 SQLite FTS5 建表/查询代码 |
Search.php | 84-139 | searchFTS() 整个方法(仅 SQLite) |
Search.php | 291-319 | syncThreadFTS() 中的 SQLite 分支 |
Search.php | 324-352 | syncBlogFTS() 中的 SQLite 分支 |
Admin/MaintenanceController.php | 18-36 | "rebuild_fts" 操作中的 SQLite FTS5 代码 |
Admin/DashboardController.php | 18-22 | SQLite 数据库大小统计代码 |
Admin/DatabaseController.php | 10,32-35,52-55,68-78,94-95 | SQLite 备份/导入/优化代码 |
Admin/LevelController.php | 11-20 | SQLite 建表分支 |
Settings.php | 311-319 | updateOnlineStatus() 中的 SQLite 分支 |
Tag.php | 60-63 | saveForThread() 中的 MySQL/SQLite 分支判断 |
Setting.php (Model) | 12,20 | getDbType() === 'mysql' 条件判断(始终为 true) |
BlogController.php | 33,65 | DATE_FORMAT vs strftime 条件分支 |
5. 冗余/死代码
5.1 废弃/备用文件
| 文件 | 说明 | 建议 |
|---|---|---|
convert.php | 数据库转换工具(自述未使用) | 删除 |
dbconvert.php | 独立数据库转换(自述保留参考) | 删除或移至文档 |
app/Controllers/Admin/DashboardController.php.bak | 备份文件 | 删除 |
app/Views/admin/index.php.bak | 备份文件 | 删除 |
5.2 冗余类
| 文件 | 说明 |
|---|---|
app/Core/View.php | 旧版视图引擎,与 TemplateCompiler.php 功能重叠。虽然仍在使用(Router 404 页引用),但已基本被 TemplateCompiler 取代 |
app/Models/OnlineUser.php | 空模型类,仅定义 $table |
app/Models/PointsLog.php | 空模型类,仅定义 $table |
app/Models/ThreadTag.php | 空模型类,仅定义 $table |
app/Models/ThreadVote.php | 空模型类,仅定义 $table |
app/Models/PostVote.php | 空模型类,仅定义 $table |
app/Models/Attachment.php | 空模型类,仅定义 $table |
app/Models/Tag.php (Model) | 仅有一个方法,与 Helpers/Tag.php 功能重叠 |
5.3 Controller.php 中的冗余方法
protected function currentUser() // 仅调用 Auth::getCurrentUser()
protected function isLoggedIn() // 仅调用 Auth::isLoggedIn()
protected function isAdmin() // 仅调用 Auth::isAdmin()这些方法只是对 Auth 类的简单包装,增加了不必要的层。建议直接使用 Auth::xxx() 或保留一对一致的命名。
6. 设计问题与最佳实践
6.1 Controller 承担过多职责
ThreadController::show() 同时处理:
- 显示帖子(GET)
- 发表回复(POST)
- 附件上传
- 积分奖励
- FTS 同步
违反 SRP(单一职责原则),建议将 POST 回复逻辑拆分为独立方法。
6.2 大量直接 SQL 查询散布在 Controller 中
多个控制器绕过 Model 层直接使用 Database::getInstance()->fetchAll(...):
ForumController.php— 多处HomeController.php— 多处ProfileController.php— 多处AuthController.php— 多处Admin/UserController.php— 几乎全部Admin/CategoryController.php— 几乎全部Admin/ThreadController.php— 几乎全部
违背 MVC 分层设计,增加维护成本和 SQL 注入风险面。
6.3 没有统一的分页参数校验
分页参数分散处理,有些地方用 max(1, ...),有些地方用 max(5, ...),没有统一的分页中间件。
6.4 ondelete 级联策略待确认
Schema 中 attachments 表对 thread_id 和 post_id 使用 ON DELETE CASCADE,但删除附件文件(物理文件)的代码分散在多处(UserController、ThreadController),容易遗漏。
6.5 Theme 删除使用的反射
文件: app/Controllers/Admin/ThemeController.php 第 138-141 行
代码:
$ref = new \ReflectionClass(Theme::class);
$prop = $ref->getProperty('cachedThemes');
$prop->setAccessible(true);
$prop->setValue(null, null);问题: 使用反射修改私有属性是一种侵入式做法。建议在 Theme 类中添加一个公开的 clearCache() 方法。
7. 总结
优先级评估
| 优先级 | 问题 | 影响 |
|---|---|---|
| 🔴 P0-紧急 | Model.php::$this->dbType 未定义 | 所有 insert 操作崩溃 |
| 🔴 P0-紧急 | Captcha.php 使用 ON CONFLICT 而非 MySQL 语法 | 验证码功能初始化失败 |
| 🟠 P1-严重 | Schema 缺少 deleted_at, last_reply_at, user_id 等字段 | 软删除、附件清理等功能失效 |
| 🟠 P1-严重 | SQLite 残留代码近 12 处 | 项目洁癖问题,但部分 SQLite 分支不会执行,不影响运行 |
| 🟡 P2-警告 | settings 表缺少允许发帖间隔、附件开关等配置字段 | 未来扩展需要 |
| 🟢 P3-建议 | 冗余类、备份文件、直接 SQL | 维护性降低 |
建议修复顺序
- 立即 —
Model.php的insert()方法(致命 bug) - 立即 —
Captcha.php的ON CONFLICT语法 - 尽快 — 补充数据库缺失字段(
deleted_at,last_reply_at等) - 尽快 — 清理所有 SQLite 残留代码(约 12 处)
- 尽快 — 修复
UserController中的attachments.user_id查询 - 建议 — 删除备份文件和冗余类
- 建议 — 添加
Theme::clearCache()公开方法替代反射
报告由 AtomCode 自动生成,基于对项目代码的静态分析。