[审计]SoSite 代码审查报告

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

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:7月5日复制的资料/代码审查报告.md。

SoSite 代码审查报告

审查日期:2026-07-01

审查范围:PHP 后端核心代码(Core、Helpers、Models、Controllers、Config 等)

审查目标:安全性、逻辑正确性、冗余代码


目录

  1. 严重 Bug(可能导致崩溃或数据损坏)
  2. 安全漏洞
  3. 数据库 Schema 与实际代码不匹配
  4. SQLite 残留代码(本应已清理)
  5. 冗余/死代码
  6. 设计问题与最佳实践
  7. 总结

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_atThread::softDelete(), getDeleted(), countDeleted(), softDelete()
last_reply_atThread::getLatest(), getLatestPaginated(), getByCategoryPaginated(), update()
is_pinnedThread::getLatest(), getLatestPaginated(), getByCategoryPaginated()
is_highlightedAdmin/ThreadController
colorAdmin/ThreadController
reply_to_viewThreadController::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.php68-76initFTS() 中的 SQLite FTS5 建表/查询代码
Search.php84-139searchFTS() 整个方法(仅 SQLite)
Search.php291-319syncThreadFTS() 中的 SQLite 分支
Search.php324-352syncBlogFTS() 中的 SQLite 分支
Admin/MaintenanceController.php18-36"rebuild_fts" 操作中的 SQLite FTS5 代码
Admin/DashboardController.php18-22SQLite 数据库大小统计代码
Admin/DatabaseController.php10,32-35,52-55,68-78,94-95SQLite 备份/导入/优化代码
Admin/LevelController.php11-20SQLite 建表分支
Settings.php311-319updateOnlineStatus() 中的 SQLite 分支
Tag.php60-63saveForThread() 中的 MySQL/SQLite 分支判断
Setting.php (Model)12,20getDbType() === 'mysql' 条件判断(始终为 true)
BlogController.php33,65DATE_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维护性降低

建议修复顺序

  1. 立即 — Model.php 的 insert() 方法(致命 bug)
  2. 立即 — Captcha.php 的 ON CONFLICT 语法
  3. 尽快 — 补充数据库缺失字段(deleted_at, last_reply_at 等)
  4. 尽快 — 清理所有 SQLite 残留代码(约 12 处)
  5. 尽快 — 修复 UserController 中的 attachments.user_id 查询
  6. 建议 — 删除备份文件和冗余类
  7. 建议 — 添加 Theme::clearCache() 公开方法替代反射

报告由 AtomCode 自动生成,基于对项目代码的静态分析。

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

全部回覆 (0)

暫無回覆

請 登入