[审计]SoSite V3.5 代码审查报告

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

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:备忘md/CODE_REVIEW.md。

SoSite V3.5 代码审查报告

审查时间:2026-06-26

审查范围:全项目 PHP 代码(核心框架、业务逻辑、插件、配置、安装脚本)

分级标准:严重 = 可被直接利用的安全漏洞;高危 = 需特定条件触发但影响大;中危 = 逻辑缺陷或潜在安全隐患;低危 = 代码质量/可维护性问题


一、安全问题

S-01 严重 — RateLimiter 类缺失导致注册/投票/上传限流失效

文件:app/Controllers/AuthController.php:8、app/Controllers/ApiController.php:9

use app\Helpers\RateLimiter 被引用,但 app/Helpers/RateLimiter.php 不存在。这会导致:

  • AuthController::register() 调用 RateLimiter::hit() 时触发 Fatal Error
  • ApiController::vote()、upload()、fetchImage() 同理
  • 注册限流(每小时3个)完全无效,可被批量注册机器人直接绕过
  • 投票限流、上传限流、远程拉图限流均失效

建议:创建 app/Helpers/RateLimiter.php,基于 Session 或 SQLite 实现 IP/用户级计数器。


S-02 严重 — Search::syncThreadFTS / syncBlogFTS SQL 注入

文件:app/Helpers/Search.php:310-311、338-339

$conn->exec("DELETE FROM threads_fts WHERE rowid = {$threadId}");
$row = $conn->querySingle("SELECT title, content FROM threads WHERE id = {$threadId}", true);

$threadId 直接拼入 SQL,虽调用方做了 (int)$id 转换,但这是纵深防御缺失——如果未来任何调用路径遗漏了类型转换,即产生 SQL 注入。且同一文件其他地方均使用了参数绑定,此处不一致。

建议:改用参数绑定:

$stmt = $conn->prepare("DELETE FROM threads_fts WHERE rowid = :id");
$stmt->bindValue(':id', $threadId, SQLITE3_INTEGER);
$stmt->execute();


S-03 高危 — 主题上传 ZIP 解压无路径穿越校验(Zip Slip)

文件:app/Controllers/Admin/ThemeController.php:96

$zip->extractTo($targetDir);

ZipArchive::extractTo 不校验内部文件路径。恶意 ZIP 可包含 ../../config.php 这样的条目,将 PHP 文件写到项目任意位置,实现 RCE。

建议:解压前逐条校验每个条目的 realpath 是否在 $targetDir 内:

for ($i = 0; $i < $zip->numFiles; $i++) {
    $name = $zip->getNameIndex($i);
    if (strpos($name, '..') !== false || strpos($name, '/') === 0) {
        $zip->close();
        $this->redirect('/admin/themes?error=bad_zip');
    }
}
$zip->extractTo($targetDir);


S-04 高危 — install.php 写入 config.php 时 MySQL 密码明文注入

文件:install.php:117-181 的 writeConfig() 函数

define('DB_PASS', '{$dbConfig['pass']}');

用户输入的 MySQL 密码直接拼入 PHP 文件,未做转义。如果密码含 ' 或 \,会导致:

  1. PHP 语法错误,config.php 无法加载
  2. 理论上可构造特殊密码注入 PHP 代码

建议:使用 var_export() 转义:

define('DB_PASS', ' . var_export($dbConfig['pass'], true) . ');


S-05 高危 — install.php 安装步骤 AJAX 无认证校验

文件:install.php:318-328

action=run_step 端点无任何 CSRF Token 或 Session 校验,恶意页面可诱导管理员访问来触发:

  • drop 步骤会删除全部 MySQL 表或删除 SQLite 数据库文件
  • schema 步骤会重建空表
  • 即使安装已完成(.install_done 存在),run_step 仍可被调用

建议:

  1. 在 run_step 中校验 CSRF Token
  2. 安装完成后(.install_done 存在时),拒绝所有安装操作
  3. 在 executeInstallStep 的 drop 步骤中加入二次确认

S-06 高危 — Cookie 主题名未校验可导致目录穿越读取模板

文件:app/Core/View.php:59、app/Core/TemplateCompiler.php:57

if (isset($_COOKIE['site_theme']) && $_COOKIE['site_theme'] !== '') {
    return $_COOKIE['site_theme'];
}

用户可设置 Cookie site_theme=../../app/Views 来操控模板解析路径。虽然后续 resolveTemplate 会拼接 .php 后缀并用 file_exists 检查,但 realpath 仅在目录存在时获取,且 themePath 会被设为用户控制的目录。Theme::getCurrent() 虽然校验了 available 列表,但 View 和 TemplateCompiler 的 detectTheme 方法绕过了该校验,直接读 Cookie。

建议:detectTheme 中增加白名单校验:

$available = \app\Helpers\Theme::getAvailable();
if (isset($available[$theme])) return $theme;


S-07 高危 — Content::decode base64 检测过于宽松

文件:app/Helpers/Content.php:153-165

if (strlen($data) > 16 && preg_match('/^[A-Za-z0-9+\/=\r\n]+$/', $data)) {
    $decoded = base64_decode($data, true);

只要内容看起来像 base64(纯字母数字+/=)就会被解码。普通中文帖子如果碰巧匹配此模式,会被错误解码为乱码。更严重的是,攻击者可通过 base64 编码绕过 sanitizeHtml 的输入过滤——解码后内容在后续处理中可能不再经过安全过滤。

建议:

  1. 添加明确的标记前缀(如 base64:),仅对有标记的内容解码
  2. 或通过隐藏字段标记数据来源,避免猜测性解码

S-08 中危 — Plugin::hook 通过 extract($params) 注入变量到钩子文件

文件:app/Helpers/Plugin.php:104

extract($params);
include $hookFile;

钩子文件的代码在插件激活后会被 include 执行,且 extract 会将所有参数注入为局部变量。如果钩子文件的路径来自 plugin.json 的 hooks 字段,而 plugin.json 可被修改(通过 PluginController::edit),则:

  1. 修改 hooks 指向任意文件可实现文件包含
  2. extract 无 EXTR_SKIP 等限制,可覆盖已有变量

建议:

  1. extract($params, EXTR_SKIP) 防止变量覆盖
  2. 钩子文件路径校验必须在 plugins/ 目录内
  3. PluginController::edit 不应允许修改 hooks 字段

S-09 中危 — 管理后台删除主题时 themeKey 路径穿越

文件:app/Controllers/Admin/ThemeController.php:120-125

$themeKey = trim($_POST['theme_key'] ?? '');
$themeDir = dirname(__DIR__, 3) . '/assets/themes/' . $themeKey;
self::removeDir($themeDir);

$themeKey 未做路径穿越校验。如果提交 theme_key=../../protected,removeDir 会递归删除 protected 目录,导致数据库和会话全部丢失。

建议:校验 $themeKey 仅含 [a-zA-Z0-9_-]:

if (!preg_match('/^[a-zA-Z0-9_-]+$/', $themeKey)) {
    $this->redirect('/admin/themes?error=invalid');
}


S-10 中危 — PluginController::edit 显示插件 PHP 源码

文件:app/Controllers/Admin/PluginController.php:56-66

钩子文件内容和 Plugin.php 源码被读取并传给视图,如果视图中直接输出,可能泄露:

  • 数据库凭据
  • 内部 API Key
  • 业务逻辑

建议:仅在显式请求时显示源码,并对输出做脱敏处理。


S-11 中危 — Captcha 盐值硬编码

文件:app/Helpers/Captcha.php:15、app/Controllers/ApiController.php:207

$salt = 'SoSiteCaptcha2024';
$inputHash = hash('sha256', strtolower(trim($input ?? '')) . 'SoSiteCaptcha2024');

盐值硬编码且在两个文件中重复定义。攻击者知晓盐值后可离线预计算验证码哈希。

建议:将盐值存储在 settings 表或 config.php 中,每次安装随机生成。


S-12 中危 — Auth::getCurrentUser 使用全局变量做缓存控制

文件:app/Helpers/Auth.php:25-28

if ($GLOBALS['_clearUserCache'] ?? false) {
    $cached = false;
    $GLOBALS['_clearUserCache'] = false;
}

使用 $GLOBALS 进行缓存控制是一种反模式,任何代码都可以设置或干扰此全局变量,且 static $cached 在长运行进程中可能导致数据不一致。

建议:使用类静态属性替代 $GLOBALS,或引入 UserCacheManager 类。


S-13 中危 — 登出使用 GET 请求

文件:app/Controllers/AuthController.php:167、index.php:69

$router->get('/logout', [AuthController::class, 'logout']);

登出操作通过 GET 请求触发,存在 CSRF 登出风险——恶意页面可构造 <img src="/logout"> 强制用户登出。

建议:改为 POST 请求并加 CSRF 校验,或至少使用 POST。


S-14 中危 — 数据库导入未验证文件类型

文件:app/Controllers/Admin/DatabaseController.php:65-73

handleImport 直接将上传文件复制到 DB_PATH(SQLite)或执行其中的 SQL(MySQL),未验证文件扩展名或内容格式。恶意文件可替换数据库。

建议:

  1. SQLite 导入时验证文件是否为有效的 SQLite3 数据库
  2. MySQL 导入时校验文件扩展名为 .sql
  3. 导入前创建备份

S-15 低危 — 数据库查询未校验表名/列名

文件:app/Core/Model.php:79-82

$columns = implode(', ', array_keys($data));
$sql = "INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})";

$data 的键名直接拼入 SQL,如果键名来自不可信输入,可导致 SQL 注入。当前调用方均使用硬编码键名,风险较低,但缺少纵深防御。

建议:校验键名仅含 [a-zA-Z0-9_]。


S-16 低危 — session.use_strict_mode 被设为 0

文件:config.php:21

ini_set('session.use_strict_mode',0);

关闭严格模式意味着攻击者可自行设置未初始化的 Session ID(Session Fixation 攻击)。虽然登录成功后调用了 session_regenerate_id(true),但注册流程没有 regenerate。

建议:设为 1。


二、逻辑问题

L-01 高危 — 发帖事务中 Search::syncThreadFTS 可能失败导致数据不一致

文件:app/Controllers/PostController.php:49-71

$db->begin();
// INSERT thread, tag, user post_count...
Search::syncThreadFTS($threadId);  // 在事务内操作 FTS 表
$db->commit();

syncThreadFTS 内部使用 SQLite3::exec(绕过 Database 类的事务),如果 FTS 同步失败抛出异常,$db->rollback() 会回滚主事务,但 FTS 表可能已被部分修改。且 FTS 操作不应在业务事务内。

建议:将 FTS 同步移到 commit() 之后执行。


L-02 中危 — 博客评论未经过 Content::formatPostContent 处理

文件:app/Controllers/BlogController.php:53-56

$content = trim($_POST['content'] ?? '');
if ($content !== '') {
    $blogModel->execute(
        'INSERT INTO blog_comments (..., content, ...) VALUES (..., :content, ...)',
        [':content' => $content, ...]
    );

评论内容直接存入数据库,未经 Content::decode() 和 Content::formatPostContent() 处理。如果视图层直接输出,可能导致 XSS;如果视图层统一做 escape,则用户输入的 HTML 标签会变成可见文本而非被过滤。

建议:存储前调用 Content::decode(),展示时调用 Content::formatPostContent()。


L-03 中危 — 帖子删除(softDelete)未同步 FTS 索引

文件:app/Controllers/ThreadController.php:201

删除帖子时调用 $threadModel->softDelete(),但未调用 Search::syncThreadFTS() 同步搜索索引。已软删除的帖子仍会出现在搜索结果中。

建议:软删除后从 FTS 中移除对应记录。


L-04 中危 — 博客创建时 content 未通过 Content::decode

文件:app/Controllers/BlogController.php:108

$content = trim($_POST['content'] ?? '');

与 PostController 和 ThreadController 不同,博客的 create 和 edit 方法都未调用 Content::decode()。如果前端编辑器使用 base64 编码提交,博客内容会以 base64 原文存储。

建议:统一使用 Content::decode() 处理内容。


L-05 中危 — 博客删除仅删除评论和博客,未删除关联附件

文件:app/Controllers/BlogController.php:196-199

$blogModel->execute('DELETE FROM blog_comments WHERE blog_id = :bid', [':bid' => (int)$id]);
$blogModel->delete((int)$id);

博客的封面图片文件和附件未被删除,导致磁盘空间泄漏。

建议:删除前查询并删除关联的文件资源。


L-06 中危 — 管理员删除用户未清理投票、积分日志、附件等

文件:app/Controllers/Admin/UserController.php:43-49

删除用户时仅清理了 posts、threads、messages、blog_comments、blogs、users,但遗漏了:

  • thread_votes / post_votes(投票记录)
  • points_log(积分日志)
  • attachments(附件记录和文件)
  • online_users(在线记录)
  • viewed_replies(查看记录)

建议:补充清理所有关联表。


L-07 中危 — Settings::runtimeIncr/Decr 计数器可能漂移

文件:app/Helpers/Settings.php:186-212

运行时计数器使用内存缓存 + DB 原子更新,但:

  1. 并发请求时内存缓存可能不一致
  2. 管理后台直接删除帖子时未调用 runtimeDecr
  3. 没有定期重建机制(仅手动在维护页触发)

长期运行后计数器值可能与实际数据严重偏差。

建议:

  1. 增加定时自动校正(如每小时与 COUNT(*) 对账一次)
  2. 所有删除路径都要调用 runtimeDecr

L-08 低危 — Auth::getCurrentUser static 缓存在同一请求中无法刷新

文件:app/Helpers/Auth.php:17-19

static $cached = false;
if ($cached !== false) {
    return $cached;
}

初始值 false 与 "未缓存" 语义混淆——如果 getCurrentUser 返回 null(未登录),下次调用会直接返回 null 而不重新查询。登录流程必须依赖 clearUserCache 才能刷新。

建议:使用独立的 $initialized 标志:

static $initialized = false;
static $cached = null;
if ($initialized) return $cached;
$initialized = true;


L-09 低危 — ThreadController::show 回帖缺少 requireLogin

文件:app/Controllers/ThreadController.php:26-28

POST 提交回复时未检查登录状态。虽然后续 $this->currentUser() 会返回 null 导致报错,但应该在方法入口就拦截。

建议:在 POST 分支开始处加 $this->requireLogin()。


L-10 低危 — View 和 TemplateCompiler 的 detectTheme 重复实现

文件:app/Core/View.php:48-68、app/Core/TemplateCompiler.php:48-66

主题检测逻辑被复制了两次(含数据库查询),且行为不一致(View 的 detectTheme 不校验白名单,TemplateCompiler 的也不校验)。两份代码需要同步维护。

建议:统一调用 Theme::getCurrent(),删除重复的 detectTheme。


三、代码质量问题

Q-01 Model::buildWhere 传入字符串条件无参数绑定保护

文件:app/Core/Model.php:152-156

if (is_string($conditions)) {
    return $conditions;
}

当 $conditions 是字符串时直接拼入 SQL,调用方若传入用户输入则产生 SQL 注入风险。虽然当前所有调用方都使用硬编码条件,但接口设计不安全。

建议:废弃字符串条件支持,或增加明确的注释警告。


Q-02 Controller::redirect 未校验 URL 来源

文件:app/Core/Controller.php:84-88

protected function redirect($url) {
    session_write_close();
    header('Location: ' . $url);
    exit;
}

基础 redirect 方法不做 URL 校验。虽然 ApiController::safeReferer 做了校验,但大部分控制器直接使用 $this->redirect('/...') 硬编码路径,当前安全。但如果未来有用户可控的重定向场景,可能产生开放重定向漏洞。


Q-03 模板编译缓存使用 md5 作 key 无防碰撞

文件:app/Core/TemplateCompiler.php:207

$cacheKey = 'tpl_' . md5($htmlFile) . '.php';

使用 md5 做缓存 key,理论上存在碰撞风险(两个不同模板文件生成相同缓存名)。

建议:使用 sha256 或直接用相对路径的 hash。


Q-04 View::icon / TemplateCompiler::icon 重复定义

文件:app/Core/View.php:240-305、app/Core/TemplateCompiler.php:319-384

图标映射表被完整复制了两遍,且 View::icon 中 calendar 键重复定义。

建议:抽取为共享 Trait 或独立类。


Q-05 Content::sanitizeHtml 中 PHP 代码标记还原未充分防护

文件:app/Helpers/Content.php:97-109

$clean = str_replace(
    ['<?php', '<?=', ...],
    ['<?php', '<?=', ...],
    $clean
);

在 HTML 输出中还原 <?php 和 <?= 标记。虽然此时已不在 PHP 执行上下文(仅存入数据库),但如果这段内容后续被 include 或 eval,PHP 代码会被执行。且还原时是原文还原而非实体转义,浏览器可能错误解析。

建议:还原为 &lt;?php 和 &lt;?=。


Q-06 Search::searchLIKE 搜索结果未过滤已删除帖子

文件:app/Helpers/Search.php:241

$cnt = $db->fetchOne('SELECT COUNT(*) as count FROM threads WHERE title LIKE :q1 OR content LIKE :q2', ...);

LIKE 搜索和 FTS 搜索均未加 AND deleted_at IS NULL 条件,已软删除的帖子仍出现在搜索结果中。


Q-07 Upload::file 返回的 type 字段来自客户端

文件:app/Helpers/Upload.php:69

'type' => $file['type'] ?? '',

$file['type'] 来自客户端 Content-Type,可被伪造。应使用 finfo 检测到的 $mime 替代。


四、架构/部署问题

A-01 install.php 安装完成后仍可访问

虽然 install.php 第18行检查了 .install_done 锁文件,但 run_step 端点(第318行)未做此检查。且 .install_done 仅在最后一步创建——如果安装中断,lock 文件不存在,安装可被重新执行(包括删库操作)。

建议:所有安装端点统一检查 .install_done。


A-02 protected/ 目录未在 .htaccess 中限制 .php 文件执行

文件:.htaccess:22

RewriteRule ^protected/ - [F,L,NC]

虽然拒绝了直接 HTTP 访问,但如果服务器配置不当(如 protected/cache/templates/ 下的编译缓存 .php 文件被直接访问并执行),可能导致代码注入。缓存文件中包含用户输入的编译结果。

建议:在 protected/ 下放置 .htaccess,Require all denied。


A-03 数据库备份文件可被下载

protected/backups/ 目录下的 .db 和 .sql 备份文件虽然被 .htaccess 的 RewriteRule ^protected/ 拒绝,但如果 Nginx/Apache 配置了静态文件直接服务(绕过 .htaccess),备份文件可被下载,泄露全部用户数据。

建议:

  1. 备份文件名添加随机 token
  2. 备份目录放置 .htaccess 拒绝访问
  3. 备份完成后可考虑加密

五、问题汇总

编号级别类别概要文件
S-01严重安全RateLimiter 类缺失,限流全部失效AuthController, ApiController
S-02严重安全FTS 同步 SQL 拼接注入Search.php
S-03高危安全ZIP 解压路径穿越Admin/ThemeController.php
S-04高危安全config.php 密码注入install.php
S-05高危安全安装 AJAX 无认证install.php
S-06高危安全Cookie 主题名目录穿越View.php, TemplateCompiler.php
S-07高危安全base64 解码过于宽松Content.php
S-08中危安全extract 注入变量到钩子Plugin.php
S-09中危安全删除主题路径穿越Admin/ThemeController.php
S-10中危安全插件源码泄露Admin/PluginController.php
S-11中危安全Captcha 盐值硬编码Captcha.php, ApiController.php
S-12中危安全全局变量缓存控制Auth.php
S-13中危安全登出使用 GETindex.php
S-14中危安全数据库导入未验证Admin/DatabaseController.php
S-15低危安全Model 列名未校验Model.php
S-16低危安全session 严格模式关闭config.php
L-01高危逻辑FTS 在事务内操作PostController.php
L-02中危逻辑博客评论未处理内容BlogController.php
L-03中危逻辑软删除未同步搜索ThreadController.php
L-04中危逻辑博客内容未 decodeBlogController.php
L-05中危逻辑博客删除未清附件BlogController.php
L-06中危逻辑删除用户数据不完整Admin/UserController.php
L-07中危逻辑计数器可能漂移Settings.php
L-08低危逻辑缓存语义混淆Auth.php
L-09低危逻辑回帖缺少登录检查ThreadController.php
L-10低危逻辑detectTheme 重复实现View.php, TemplateCompiler.php
Q-05低危质量PHP 标记还原不安全Content.php
Q-06低危质量搜索未过滤已删帖Search.php
Q-07低危质量上传 type 来自客户端Upload.php
A-01高危部署安装端点无锁校验install.php
A-02中危部署缓存 PHP 可执行protected/cache/
A-03中危部署备份文件可下载protected/backups/

总计:严重 2 个,高危 6 个,中危 14 个,低危 10 个


六、优先修复建议

  1. 立即修复 S-01(创建 RateLimiter 类)、S-02(FTS 参数绑定)、S-03(ZIP 解压校验)、S-05(安装端点加锁)、S-06(主题白名单校验)
  2. 尽快修复 S-04(config.php 转义)、S-07(base64 标记前缀)、S-09(主题名正则校验)、L-01(FTS 移出事务)
  3. 迭代修复 其余中危和低危问题
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| Views 4 | Replies 0

All Replies (0)

No replies yet

Please Log in