[修复]全量代码审计与四批次修复实施报告

👑Lv.11 元老 🌞 骨干会员
2026-10-23 23:43:30

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:合并篇(见正文内 merged_from 列表)。

全量代码审计与四批次修复实施报告

本篇由 9 篇原始文档合并去重而成:5 篇分层全量审计报告(Core / Controllers / Models / Helpers / SplitDB)+ 1 篇逐条复核报告 + 3 篇批次修复实施报告(P0+P1 / P2 / P3)。

合并口径:以复核定过级的结论为准,原始报告的重复条目只保留一行;修复情况按批次归并,不再逐篇罗列。

一、总览

2026-09-02 一天之内完成「五层全量静态审计 → 逐条人工复核定级 → 四批次修复落地」。

阶段内容产出
审计app/ 下 Core / Controllers / Models / Helpers / SplitDB 五层逐行静态审查,不运行代码Core 26 项、Controllers 15+ 项、Models 29 项、Helpers 11 项、SplitDB 18 项
复核对每条发现回到当前磁盘代码取证(文件:行号 + 代码摘录),结合跨层调用链与 Schema 判断真实影响三态归类:确认属实 / 属实但评级需降 / 不属实或已被现状解决
修复P0+P1 一批、P2 一批、P3 四小批合计修复 8 + 12 + 20 = 40 项,跳过 3 项(均注明原因)

回归基线: cli/smoke_frontend_flow.php 修复前后均为 21 通过 / 0 失败;cli/smoke_queue_consumer.php = 11 通过 / 0 失败。

修复过程纪律: 只改代码不动真实业务数据;回归用独立临时数据冒烟脚本;临时探针脚本用完即删。

二、五层审计发现清单

2.1 Core 层(app/Core/ 6 个文件,约 73 KB,26 项)

编号问题复核结论
H-01TemplateCompiler {loop} 捕获组 [^\s]+? 允许括号,{loop $getUsers() $x} 编译为 foreach,与已删的 {php} 同级函数调用向量属实,评级降为中危·潜在 RCE(模板来自 app/Views/ 与 assets/themes/ 本地文件,后台主题上传/删除已禁用,当前部署不可达;仍作纵深防御修复)
H-02Model::buildWhere 字符集正则放行括号/逗号/OR,注释却称"禁止任意原始 SQL"属实,降为中危·设计缺陷(现存调用方均走参数绑定,未发现拼接用户输入路径)
M-01CSP 允许 unsafe-inline / unsafe-eval属实,维持中危
M-02update() 空数据生成 UPDATE t SET WHERE 语法错误属实,维持中危
M-03表名/主键未做标识符引用,仅 insert 列名加引号属实,降低危
M-04{yield default} 默认值原样拼接进 PHP与 H-01 同源,随 H-01 一并收紧
M-05pagination() 的 $url 未做协议校验属实,实际风险低(调用方均为写死常量),降低危
M-06Router 插件钩子无外层 try-catch部分属实——Plugin::hook() 内部已对每个钩子文件 include 做 try-catch(Plugin.php:243-248),缺的只是外层兜底,降低危
L-01 ~ L-08自动加载无路径校验、viewRaw() 缺安全头、json() 无 nosniff、错误页硬编码、count() 冗余、icon() extraClass 未转义、模板缓存目录 0755、模板缺失输出注释泄路径全部属实,维持低危
A-01 ~ A-05Database 单例紧耦合、Model 与 Database 紧耦合、TemplateCompiler 953 行职责过重、安全头重复、注释与行为不一致属实,架构建议级
P-01 ~ P-03icon 静态映射解析开销、无查询结果缓存、路由线性匹配属实但收益极小 / 可忽略

2.2 Controllers 层(根目录 12 个 + Admin 16 个,15+ 项)

编号问题复核结论
H-1AttachmentController::download 通过 LEFT JOIN business.threads 取版块,SplitDB 迁移后该表已停写 → JOIN 恒 NULL → category_id=0 → 权限校验整段跳过✅ 属实,本轮最值得先修的发现。游客可下载任意私密版块附件
M-1DashboardController::index 查已退役 threads 表,仪表盘"最新帖子"恒空✅ 属实(功能缺陷)
M-2TagsController::show 权限过滤在分页之后执行,分页数据不一致属实
M-3Admin/VerifyController::index 管理员二次验证无频率限制属实
L-1 ~ L-5自定义 CSS 过滤不完整、ThemeController::removeDir 死代码、批量操作 ID 未 intval、插入与计数无事务、备份下载 GET 无 CSRF属实
B-1 ~ B-3用户搜索总数硬上限 50、$latestThreads 未显式初始化、未检查收件人拒收私信属实(业务逻辑)
Q-1 ~ Q-3分页逻辑大量重复、SettingsController 方法体过长、ApiController 职责过多质量改进级

2.3 Models 层(app/Models/ 10 个模型,29 项)

等级条目
Critical 5C-1 删除用户附件删除路径遍历;C-2 未清理用户自己的回复(桶行+extern+reply_index 全残留);C-3 defaultAvatarPath 调 mt_srand 污染全局随机种子;C-4 分片写入无事务保护;C-5 User::search LIKE 通配符转义变量未使用
High 8H-1 countByThread 未过滤软删;H-2 删最后回复后 last_reply_time 不回退;H-3 非 sync 模式下 total_threads 永不更新;H-4 Category 删除单版块场景指向已删版块;H-5 BlogCategory 删除无事务;H-6 更新正文旧 extern 文件不删;H-7 Thread::update 空字段 SQL 语法错误;H-8 hardDelete 在事务外执行
Medium 8M-1 桶容量只统计 topic;M-2 Model 层直接调 Controller(分层倒挂);M-3 batchFetch 表名列名裸拼;M-4 缓存注释与实现不符;M-5 softDelete 无状态检查;M-6 软删未调 total_posts;M-7 Setting::setValue 先查后插竞态;M-8 邮箱 Token 无过期校验
Low 8L-1 缓存路径策略不一致;L-2 无意义 public 包装;L-3 注释编号重复;L-4 废弃方法未清理;L-5 decorate 输出结构不一致;L-6 循环内逐个读 extern 文件 N 次 IO;L-7 无缓存;L-8 batchDelete 未清关联数据

复核中已明确判定:M-8 不属实(AuthController 已有 24h 过期校验);L-4 不属实(getLatest/restore 有插件与回收站真实调用方)。

2.4 Helpers 层(11 项)

  • 安全维度:SQL 注入 / CSRF / 认证会话 / 加密均通过;XSS 通过但有一处需关注;文件上传存在 finfo 失败时仅凭扩展名降级放行的 polyglot 风险;权限模型与插件安全各 1 项中危。
  • 性能维度:缓存策略、查询优化、内存资源、文件 IO 均良好;1 处 N+1。
  • 质量维度:规范优秀、错误处理良好;耦合度中等;2 处重复代码;若干死代码与全局函数问题。

2.5 SplitDB 层(18 项)

  • H-01 跨季度编辑时旧归档 .idx 按"新路径目录"作废,旧季度索引永不失效,编辑可能静默不生效。
  • H-02 单条巨型多行 INSERT 占位符数量依赖编译期 SQLITE_MAX_VARIABLE_NUMBER(默认 999),宿主长文整体回滚。
  • M-01 浏览量计数 APCu 键无 TTL 只减不删 + flush 先扣后落库(DB 失败丢计数)。
  • 其余为健壮性/架构类中低危项(M-02 起,多在本轮 P2 批次处理)。

三、复核定级:三个最关键的跨层真实问题

复核报告(对豆包五层报告的逐条回查)最终锁定三项,全部在本轮 P0/P1 修复:

问题跨层表现风险
附件下载权限绕过Thread::insert 早已不写 business.threads(只写分片桶 + main_index),但 AttachmentController::download 仍 LEFT JOIN threads 取 category_id新帖附件 category_id=0,权限校验整体被跳过,私密版块附件可被游客直接下载
模板注入 → 任意函数调用{php} 已删,但 {loop} 数组变量与 {yield default} 默认值未做表达式白名单主题/插件模板一旦可被写或由不可信来源引入,即有 RCE 风险
分片写入无补偿 + 退役表仍被读正文/桶/索引多步写入无事务回滚;business.threads/posts 已停写却仍被 Dashboard、附件下载等读取"写入走新路、读取走旧路"的静默数据漂移

四、修复实施

4.1 P0 + P1 批次(共 8 项,全部修复并验证)

P0 —— 安全 / 越权 / 模板注入(3 项)

项问题改法涉及文件
P0-1附件下载权限绕过(见 §三-1)附件行查询去掉 LEFT JOIN threads;版块归属改从 main_index.topic_index 按 thread_id 单查 category_id;校验收紧为白名单式——categoryId <= 0(含 thread_id 为空或索引无行)或 can_view/can_attach 任一不通过即返回 403,不再默认放行。basename + realpath 双重路径防护保留app/Controllers/AttachmentController.php
P0-2{loop} 函数调用向量捕获组由 [^\s]+? 收紧为 [a-zA-Z_][a-zA-Z0-9_\[\]\'"\->]*(标识符 + 数组下标 + 引号 + 属性访问),不含括号与空白;函数调用形态不再匹配该标签,留在输出文本中不编译。三形 loop 与 fixArrayKeys 行为不变app/Core/TemplateCompiler.php
P0-3{yield default} 表达式注入cbYieldDefault 重写——默认值一律视为文案:引号包裹且闭合的先去外层引号、未加引号的按纯文本处理,统一反斜杠/单引号转义后以单引号字符串字面量输出;system('id') 只作文本不执行app/Core/TemplateCompiler.php

P1 —— 数据一致性 / 防崩溃 / 功能缺陷(5 项)

项问题改法涉及文件
P1-1仪表盘"最新帖子"恒空改读 main_index.topic_index(status=0 AND deleted_at IS NULL ORDER BY create_time DESC, id DESC LIMIT 10),随后两条 IN(...) 批量补 users.username 与 categories.name(一次请求 3 条查询,无 N+1);清理无用 useapp/Controllers/Admin/DashboardController.php
P1-2删用户后其在他帖下的回复成孤儿Thread::hardDelete 循环后新增兜底:按 reply_index WHERE uid = 被删用户 取残留 → 按 bucket_path 分组 → 逐桶 DELETE reply 行 + ExternStorage::delete 删正文 + DELETE reply_index 行,并按条数 runtimeDecr('total_posts') 保持全站计数一致app/Models/User.php
P1-3Category 删除 fallback 指向自身 / 唯一版块误删fallback 排除自身;唯一版块拒绝删除;补三语文案app/Models/Category.php、app/Controllers/Admin/CategoryController.php、lang/{zh,en,zh_tw}.php
P1-4编辑正文后旧 extern 文件不删更新时删除旧正文文件,写入新文件app/Models/Thread.php、app/Models/Post.php
P1-5User::search LIKE 通配符转义未生效转义后的变量真正参与绑定app/Models/User.php

P0+P1 验证: 11 个改动文件 php -l 全部无语法错误;{loop} 编译探针(临时脚本,已删)确认合法形态正常编译、{loop $fn() $x} 等攻击形态全部拒绝;{yield default} 探针确认 system('id') 只作纯文本;冒烟脚本 21 通过 / 0 失败,无回归。

4.2 P2 批次(健壮性 / 架构 / 性能,15 项中 12 修 3 跳)

项问题改法涉及文件
P2-①buildWhere 校验形同虚设改白名单解析器:仅允许「[表前缀.]标识符 比较运算符 :占位符」以 AND 连接;其余(OR/IN/子查询/字面量/函数)一律抛 InvalidArgumentException,fail-closedapp/Core/Model.php
P2-③搜索两轮逐帖 find() 的 SQL N+1候选扫描与输出均改一次 Thread::getByIds() 批量取基行;正文由新增 Thread::fillExternContent() 按桶分组批量查 extern_path + 读文件回填;输出顺序/_score/高亮/摘要与旧逻辑一致app/Helpers/Search.php、app/Models/Thread.php
P2-④finfo 失败时非图片仅凭扩展名放行finfo 不可用 → 所有类型拒绝上传 + error_log 严重日志,删除降级放行分支app/Helpers/Upload.php
P2-⑥跨季度/归档后编辑,旧 .idx 永不失效ExternStorage::write() 新增可选 $oldRelPath,作废按旧 relPath 所在季度目录定位;Thread/Post::update 均传旧 extern_path;抽 atomicRename() 统一两处 tmp+rename 的 Windows 兜底app/SplitDB/ExternStorage.php、app/Models/Thread.php、app/Models/Post.php
P2-⑦巨型 INSERT 依赖 SQLITE_MAX_VARIABLE_NUMBER按 150 token/条分块 INSERT(占位符 ≤750,低于默认 999),同一事务,任一块失败整体回滚app/SplitDB/SearchIndexStore.php
P2-⑧ViewCounter:apcu 键永驻、flush 先扣后落、归零键不删inc() 传 TTL 3600s;flush() 改事务内先落库、COMMIT 成功后统一 apcu_dec,归零键 apcu_delete;DB 失败回滚时计数保留下窗口重试app/SplitDB/ViewCounter.php
P2-⑨Post 直接调 Controller(Model→Controller 反向依赖)新建 BucketAutoScaler.php 承接 currentBucketSize / updateBucketSize(+upsertDefine) / emergencyExpand;Post 改调服务类;DatabaseController 三方法降为薄委托app/SplitDB/BucketAutoScaler.php(新增)等 3 个文件
P2-⑩DBFactory:closeAll 无效代码;evictLRU 淘汰前不查活动事务删无效 foreach;淘汰前检查 inTransaction(),最久未用者在事务中则改挑非事务句柄,全在事务中则放弃淘汰app/SplitDB/DBFactory.php
P2-⑪队列 done/failed 永不清理;recoverTimeout 仅空闲触发新增 purgeCompleted($retentionSeconds)(默认 7 天);consume() 忙碌与空闲路径均按 MAINTENANCE_INTERVAL=60s 节流维护;构造器新增 retention 参数(默认 604800,向后兼容)app/SplitDB/QueueConsumer.php
P2-⑫回复容量守卫只统计 topic 行数改 topic + reply 双 COUNT(reply 与 topic 同桶同库),60s 进程内缓存与 90%/100% 阈值语义不变app/Models/Post.php
P2-⑬统计缓存路径不一致 + docblock 谎称 60s 缓存新增 categoryStatsPath() 统一到 data/runtime/category_stats.cache;invalidate 与 get 读写同路径(防失效落空);保留旧路径一次性兼容清理;3 处 docblock 纠偏为"永久文件缓存 + 请求内静态缓存(发帖/回帖主动失效)"app/Models/Category.php
P2-⑭Setting::setValue 先查后插竞态改单条 UPSERT:INSERT … ON CONFLICT("key") DO UPDATE SET value=excluded.value(SQLite 3.39 ≥ 3.24,已实测)app/Models/Setting.php

P2 跳过 3 项(均注明原因)

项建议动作跳过原因
P2-②CSP 移除 unsafe-eval会破坏现有功能:assets/lib/fhstate.js(Alpine.js v3.x)依赖 new Function + with(scope) 运行时编译指令,全站视图大量使用;移除将令全部 Alpine 交互失效。需先迁移 CSP 编译版或改原生 JS,属独立改造项
P2-⑤Permission 默认放行收敛会破坏现有功能:getAuthorizedCategoryIds() 在权限表整体非空后按"该组有 can_view 行"过滤,历史版块无行;自动插默认记录会让表由空转非空,所有无行旧版块从授权列表剔除。安全落地需存量数据迁移 + 后台权限页语义变更,属产品策略项
P2-⑮Thread::softDelete 同步删 thread_tags会破坏恢复功能:Thread::restore 只重置 status/deleted_at、不重挂标签,软删即删标签会让误删帖恢复后永久丢标签。更安全路径是在 Tag 查询端过滤,留作后续单独项

4.3 P3 批次(代码卫生 / 低危,四个小批)

第一批 · 安全头与响应一致(3 项)

  • P3-1 新增 Controller::setSecurityHeaders()(nosniff / XFO / Referrer-Policy / CSP / Permissions-Policy / 条件 HSTS),view() 与 viewRaw() 共用;json() 补 nosniff。
  • P3-2 Router 的 route_before_dispatch 钩子补 try-catch + error_log 隔离。
  • P3-3 备份下载由 GET 链接改 POST 表单 + CSRF token(Csrf::verifyOrDie),视图按钮同步改 POST 表单。
  • 关于一次性 token:项目 Csrf 为 30 分钟会话级、verify() 不轮换(防并发 AJAX 403),且数据库管理页多操作共享同一渲染 token;下载后强制轮换会令同页其余表单失效,故采用会话级 token,"轮换为一次性"留作后续项。

第二批 · 防御性收紧(3 做 1 跳过)

  • P3-4 Model 基础 CRUD 的表名与主键统一加双引号标识符引用(子类 $table 均为裸标识符、无 $primaryKey 覆盖,已 grep 确认无保留字风险)。
  • P3-5 Admin 批量操作 ID 在控制器层 array_map('intval') + 过滤 ≤0(batch_move 走基类 batchMoveIds,内部已 intval+500 上限,不重复加)。
  • P3-6 User::countSearch() 独立 COUNT;用户 Tab 总数改用 countSearch(不再受 50 截断影响),深页分页可达。
  • ⏭️ 跳过 Upload finfo 全拒:P2-④ 已实现,本批无需重复。

第三批 · 代码清理 + 注释纠偏(先 grep 后动)

  • ThemeController::removeDir:app/cli/plugins/update 全 grep 仅自身定义 + 自递归,无外部调用方 → 删除整个私有方法。
  • Blog::decodeRowsPublic:BlogController.php:89 有真实调用 → 不删(跳过并注明)。
  • Settings.php 空注释节"分页工具" → 删除。
  • CountingPDO.php 文件头"页脚总 SQL即为全库真实查询总数"误导注释 → 纠偏为真实口径(页脚展示 Database::getQueryCount() 业务库查询数,getTotalSqlCount() 仅 cli 诊断用),不改任何逻辑。
  • Category "60 秒" docblock 已由 P2-⑬ 全部纠偏,无残留;Thread/Post insert 步骤编号"7 出现两次"系将 7.5 误读,现码连续,均无需改动。

第四批 · 收尾低危(5 项)

  • P3-7 模板缓存目录 @mkdir 0755 → 0750(收紧 group/other 可读)。
  • P3-8 icon() 的 $extraClass 拼入 class 前 htmlspecialchars 转义(纵深防御)。
  • P3-9 模板缺失由 echo "<!-- Template not found -->" 改为静默失败 + error_log(不再向页面输出内部模板结构)。
  • 另含 CSP 相关与响应一致性收尾项(随第一批口径统一)。

P3 验证: 每小批各跑一次 cli/smoke_frontend_flow.php,四次均 21 通过 / 0 失败;queue 冒烟 11 通过 / 0 失败。

五、验证结论

验证项结果
改动文件 php -lP0+P1 11 个、P2 14 个文件全部无语法错误
buildWhere 白名单解析器合法形态("key" = :key、username = :username、t.id = :id AND t.status = :status)全部放行;注入形态(id = 1 OR 1=1、子查询、字面量、IN、分号拼接)全部抛异常
{loop} / {yield default} 编译探针合法形态正常编译,攻击形态被拒或降级为纯文本(探针为临时脚本,已删)
cli/smoke_frontend_flow.php修复前 21/0 → 修复后 21/0,无回归;覆盖发帖/搜索/回帖/投票/标签全链路
cli/smoke_queue_consumer.php11/0;覆盖 P2-⑪ recoverTimeout/purge/drain
extern 回写核对(P2-⑥)Thread::update / Post::update 均仍在每次正文编辑后回写 extern_path,无"编辑后仍读旧文件"路径

六、明确未动(延续约定)

  • M-8:邮箱 Token 过期——AuthController 已强制 24h 校验,原报告判"无过期机制"不属实。
  • L-4:Thread::getLatest / Post::restore——存在 cli、插件、回收站真实调用方,原报告判"废弃方法"不属实。
  • L-08(SplitDB):页脚已移除总 SQL 展示,原报告依据的是过期注释。
  • C-3:mt_srand 影响安全随机——经核实不影响安全随机,未修改。

本轮处置边界: 未触碰真实业务数据;回归基于独立临时数据冒烟脚本;临时探针脚本用完即删。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 3 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录