[审计]安全与代码审计结果(2026-08-21)

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

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:暂存20260913/审计结果20260821.md。

安全与代码审计结果(2026-08-21)

审计范围:第三方对本次「自适应验证码防线」修改新增文件进行审计反馈,本文件依据实际代码现状逐条复核,区分「已确认」「已修复/不成立」「需加固」三类,并给出处置建议。

复核依据:app/Controllers/ApiController.php、app/Controllers/AuthController.php、app/Helpers/PoW.php、app/Helpers/RateLimiter.php、app/Helpers/Content.php、app/Helpers/Csrf.php、app/Models/User.php、app/SplitDB/Schema.php、app/Views/auth/login.php、app/Views/auth/register.php、app/Views/layouts/main.php、assets/js/pow.js、index.php

状态说明:指令为「先出文档,等指令再改」。以下结论仅用于记录与对齐,尚未对代码做任何修改。


一、修复优先级总览

#位置问题复核结论优先级
1main.php 语言切换链接REQUEST_URI 拼链接未转义 → 反射型 XSS✅ 已确认高
2fetchImage() / fetchHopByHop()SSL 证书校验关闭,支持 MITM 投毒✅ 已确认(取舍)高/中
3RateLimiter::writeIpRecords()抢锁失败直接放行 → 高并发下限流失效✅ 已确认中/高
4RateLimiter IP 来源依赖 REMOTE_ADDR,反代下误判✅ 已确认中
5RateLimiter 文件 GC无过期缓存回收,磁盘堆积✅ 已确认中
6register.phpcheckPasswordStrength 未定义防兜底✅ 已确认低/中
7login.php锁定态 captchaImg 重复事件绑定✅ 已确认低/中
8main.php 登出表单csrf 隐藏域未 $this->e() 转义✅ 已确认(低危)低
9main.php 抽屉视图层直接跑 SQL 聚合查询✅ 已确认(架构)低/中
10PoW::validate() 重放session 未 write_close 强制落盘⚠️ 理论面,需加固低
11PoW::issue()/api/pow 无自身限流✅ 已确认中
12Schema::bucketFromPath()realpath 范围未强制核验⚠️ 正则已强约束,建议加固低
13Schema::ensureProtectionFiles()仅生成 Apache/IIS 防护,Nginx 靠人工✅ 需文档提醒中
14Schema::seedDefaults()批量 INSERT 未显式事务✅ 已确认低/中
15Schema PRAGMA运行时 table_info 检查✅ 已确认(架构)低
16ApiController::vote()CSRF 拼进 HTML <script>⚠️ 建议改用 json_encode/HX-Trigger低
17postBody() 正文输出XSS(正文经净化,实际已兜底)⚠️ 已由 sanitizeHtml 覆盖中→已缓解
18邮箱验证 Token 哈希不一致审计报告重大指控❌ 不成立,已复核一致—


二、逐条复核详情

2.1 「高」修复:main.php 语言切换链接反射型 XSS — ✅ 已确认

代码依据(app/Views/layouts/main.php L142-154,移动端同理):

$i18nUri  = (string)($_SERVER['REQUEST_URI'] ?? '/');
$i18nBase = (string)preg_replace('/([?&])lang=[^&]*&?/', '$1', $i18nUri);
...

REQUEST_URI 未经 htmlspecialchars 转义即拼入 href。若 url() 内部不自动转义(需确认 app/Core/Controller::url()),攻击者可构造 /?module=" autofocus onfocus=alert(1) 之类的 URI 造成属性注入/反射型 XSS。

处置建议:输出前统一 $this->e($this->url(...));或在进入链接前对 preg_replace 后的 $i18nBase 做防属性注入清洗(剔除 " ' < >)。


2.2 「高/中」:fetchImage() / fetchHopByHop() SSL 校验关闭 — ✅ 已确认

代码依据(ApiController.php L299-302、L340-341):

'ssl' => [ 'verify_peer' => false, 'verify_peer_name' => false, 'timeout' => 15 ],
// cURL 分支
CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => 0,

为满足「Host 替换为 IP + 保留原始 SNI」的 SSRF 方案,关闭了证书校验。这使中间人在该链路可伪造证书、篡改图片数据或重定向响应。

补充判断(审计中一项不成立):审计称 foreach ($resolvedIps as $ip) ... $targetIp = $ip; 存在「内网 IP 排在最后可被绕过」。经复核 —— isBlockedIp($ip) 命中时调用 jsonError(),而 jsonError() → json() → exit,任一解析 IP 为内网即直接终止,不会继续到 $targetIp = $ip;,因此不存在该绕过路径。

处置建议:

  1. 可尝试在 cURL 分支启用 CURLOPT_SSL_VERIFYPEER + 指定 CA,将 URL host 恢复为原始域名(依赖 DNS),与「IP 替换」方案二选一;
  2. 若必须保留 IP 直连方案,至少对 cURL 使用 CURLOPT_SSL_VERIFYPEER=false 时配合 CURLOPT_CAINFO 或友好头 Strict-Transport-Security 场景降级;
  3. 建议在注释中说明这是 SSRF 与 TLS 完整性之间的主动取舍,并记录约定。

2.3 「中/高」:RateLimiter::writeIpRecords() 抢锁失败直接放行 — ✅ 已确认

代码依据(app/Helpers/RateLimiter.php L85-99):

$fh = @fopen($file, 'c+b');
if ($fh === false) return;                       // 降级放行
if (@flock($fh, LOCK_EX | LOCK_NB)) { ... }      // 抢锁失败不写入
fclose($fh);

当并发爆破时,非阻塞锁失败即放弃写入,timestamp 未落盘 → 下一次检查仍判定未超限,限流「漏放」。

处置建议:

  1. 抢锁失败时采用有限次退避重试(usleep 抖动 2~3 次);
  2. 仍失败则回退 Session 级强制记数(已有 Session 记录为底线,不可再放行),把「完全放行」改为「降级但仍有边界」;
  3. 长期建议迁移至 Redis/APCu+文件混合。

2.4 「中」:RateLimiter IP 来源依赖 REMOTE_ADDR — ✅ 已确认

代码依据(RateLimiter.php L113/L148/L227):IP 统一取自 $_SERVER['REMOTE_ADDR']。

若站点部署于 Nginx 反代 / Cloudflare / ALB 之后,REMOTE_ADDR 恒为代理服务器地址,所有用户共享同一限流池 → 正常用户被误锁或限制被反转绕过。

处置建议:引入受信代理链位的 IP 提取函数(读取 HTTP_X_FORWARDED_FOR 并校验直连方为受信代理),在框架初始化处统一注入或封装 clientIp()。


2.5 「中」:RateLimiter 文件缓存无 GC — ✅ 已确认

代码依据(RateLimiter.php readIpRecords/writeIpRecords):每个 _ip_rate_{action}_{ip} 生成独立 ip_*.cache,无过期回收。Bots/Crawler 扫量时可堆积数十万小文件,拖慢 inode 检索。

处置建议:追加 GC,低概率(如 1%)触发过期文件清理或滚动窗口复用同一文件;长期建议换 Redis。


2.6 「低/中」:register.php 引用 checkPasswordStrength 无兜底 — ✅ 已确认

代码依据(app/Views/auth/register.php L58-60):

document.getElementById('regPassword').addEventListener('input', function(){
    checkPasswordStrength(this.value);
});

全局 checkPasswordStrength 在 register 页未显式声明/引入。若布局未载入含该函数的 JS,将持续抛 ReferenceError 并中断后续脚本。

处置建议:调用前 typeof checkPasswordStrength === 'function' 判定;或确认其在 auth.js 中全局定义且 register 页必加载。


2.7 「低/中」:login.php 锁定态下 captchaImg 重复事件绑定 — ✅ 已确认

代码依据(app/Views/auth/login.php L71 与 L78):

  • L71:$locked && $lockRemaining > 0 条件块内绑定一次
  • L78:条件块外始终再绑定一次

锁定态时同一元素被绑定 2 个 click → 每次点击触发 2 次验证码刷新,后发覆盖先发导致「验证码永远错误」。

处置建议:删除条件块内(L71)的冗余绑定,仅保留 L78 单一绑定。


2.8 「低」:main.php 登出表单 csrf 未转义 — ✅ 已确认

代码依据(app/Views/layouts/main.php L206、L324、L392):


未用 $this->e()。当前 Token 为 bin2hex(random_bytes(32))(见 Csrf.php L17)——纯 hex,实际无注入风险,但风格上不一致。

处置建议:统一补 $this->e(...),与其它表单保持一致。


2.9 「低/中」:main.php 抽屉视图层跑 SQL 聚合 — ✅ 已确认(架构)

代码依据(app/Views/layouts/main.php L106-107):

(new Blog())->query(
  "SELECT strftime('%Y-%m', created_at) as month, COUNT(*) as cnt FROM blogs GROUP BY ..."
);

主布局对全站页面加载即触发此全表分组统计,随文章量增长有性能压力,且违反 MVC 分层。

处置建议:下沉到 Controller/Service 并加 TTL 缓存,或模型层提供缓存方法后由控制器传入 View。


2.10 「中/低」:PoW::validate() 重放 — ⚠️ 理论面,需加固

代码依据(app/Helpers/PoW.php L85):

unset($_SESSION['pow_challenge'], ...); // 一次性

逻辑正确,但 PHP 默认是在脚本结束时才写回 Session。若 validate() 后控制器在 session_write_close()/正常结束前 exit/致命错误,擦除状态可能未落盘,同 challenge+nonce 可再复用。

处置建议:在确认 challenge 存在后先 session_write_close() 强制落盘;或引入全局一次性 nonce 列表(Redis/文件)。


2.11 「中」:PoW::issue() 触发 mode 判定但 /api/pow 自身无限流 — ✅ 已确认

代码依据(app/Helpers/PoW.php L40,ApiController::pow() L483-491):pow() 仅无脑签发 challenge,issue() 内部 RateLimiter::isLimited 是只读判定。攻击者可用脚本批量刷 /api/pow 冲击 Session/DB,不触发任何限流计数。

处置建议:为 /api/pow 增加 RateLimiter::hit('pow', ...);issue() 同时做读前判定即可。


2.12 「低」:Schema::bucketFromPath() 路径范围 — ⚠️ 正则已强约束,建议加固

代码依据(app/SplitDB/Schema.php L748-760):要求 bucket/(active|archive)/(\d{4}Q\d)/(\d+)\.sqlite,bucketPath 必须完全命中该锚定正则,含 .. 或非此结构均会抛异常。因此目录穿越实际难以达成。审计建议补 realpath 范围校验属于纵深防御。

处置建议:可在拼接前用 realpath($root) 做基底比对,低成本加固。


2.13 「中」:Schema::ensureProtectionFiles() 仅覆盖 Apache/IIS — ✅ 需文档提醒

代码依据(Schema.php L97 起):仅写 .htaccess 与 web.config。Nginx 依赖 nginx-server.conf 的 location deny 人工配置,无法由 PHP 自动生成,若漏配则 data/*.sqlite 可被直下。

处置建议:在 bootstrap() 检测 Web 服务器类型为 Nginx 时,向日志输出高亮安全告警;或在 data/ 根额外写一个 index.html/index.php(<?php http_response_code(403); die; ?>)作为 Nginx 默认兜底(最简单有效)。


2.14 「低/中」:Schema::seedDefaults() 批量 INSERT 未显式事务 — ✅ 已确认

代码依据(Schema.php L165 起的多个 INSERT OR IGNORE 循环 execute):每 execute 为独立事务,多进程并发 bootstrap() 时可能触发 SQLite SQLITE_BUSY。

处置建议:beginTransaction() / commit() 包裹批量写入。


2.15 「低」:Schema 运行时 PRAGMA table_info — ✅ 已确认(架构)

代码依据(Schema.php L578/L620):动态升级路径反复执行 PRAGMA table_info,打断 SQLite 语句缓存。建议仅在显式 Upgrade 操作时触发,不放入常驻检查。


2.16 「低」:ApiController::vote() 把 CSRF 拼进 htmx HTML <script> — ⚠️ 建议优化

代码依据(ApiController.php L103):

. '';

Token 为纯 hex,无攻击面,但硬编码进 HTML 脚本块属坏味道。建议改用 json_encode 输出,或经响应头 HX-Trigger/全局事件由前端统一更新。


2.17 「中→已缓解」:postBody() 正文输出 XSS

代码依据(ApiController.php L544 结合 Content.php):

echo Content::formatPostContent(Content::decode($thread['content'] ?? ''));

审计最初指控「未严格净化注入 DOM」。经复核 formatPostContent()(Content.php L270-296)对含标记内容调用 sanitizeHtml(),后者基于 DOMDocument 白名单标签/属性 + style 白名单过滤 + 协议白名单(Content.php L15-136),已有 XSS 兜底。但该净化能力高度依赖 sanitizeHtml,建议:

  1. 补充完善的单元测试覆盖恶意输入;
  2. 输出上下文本就为 htmx innerHTML,务必确保 sanitizeHtml 永不劣化(回归保护)。

2.18 「重大指控」:邮箱验证 Token 哈希不一致 — ❌ 不成立(已复核一致)

审计原文:认为 resendVerification() 把 $token 明文写入 DB,而 verifyEmail() 用哈希比对,导致功能失效。

复核结论:该指控与当前代码不符。

  • 写入侧:所有路径均经 User::setVerifyToken()(app/Models/User.php L278-284),其内部统一对 token 做 hash('sha256', $token) 后入库:

```php

public function setVerifyToken(int $userId, string $token): void {

$this->execute('UPDATE users SET email_verify_token = :token, ...',

[':token' => hash('sha256', $token), ...]);

}

```

  • 读取侧:verifyEmail()(AuthController.php L358)$hashedToken = hash('sha256', $token) 后按明文 token 哈希比对 —— 与写入侧同一标准(SHA-256)。

因此「写入明文、读取哈希」的失配并不存在,verifyEmail/resendVerification 端到端一致。若审计观察到入口路由缺失注册,是其它层面的问题(见下)。

注:本次审计未发现 verifyEmail/resendVerification 路由在 index.php 之外被调用的阻断性问题;GET /verify-email/{token}、GET /resend-verification 均已注册(index.php L151/L153)。


三、附:登录锁定(AuthController::login)复核

代码依据(AuthController.php L47-130):

  • 锁 key:_login_lock_{md5(username)},存 settings 表 JSON,跨 Session 生效(不依赖 Session),更换 session id 不可绕过 —— 设计合理;
  • 用 INSERT ... ON CONFLICT DO UPDATE 单条原子 UPSERT,比「先 SELECT 再 UPDATE」更抗并发,审计中「非原子读写导致并发覆盖」此项部分不成立;
  • 但带来的副作用是:将粒度为「用户/IP」的临时锁写入全局 settings 表,高频爆破下确实会造成该表膨胀与读写放大(合理性存疑)。
  • usleep(250000) 用于防时间侧信道枚举(密码时间差),password_verify 对不存在用户使用 dummy hash 统一耗时 —— 较好做法。

处置建议:保持原子 UPSERT 不动;将锁从 settings 迁至专用 login_attempts 表或缓存介质,避免污染全局配置表。


四、审计报告的 pow.js 与 index.php 诉求复核

诉求现状复核结论
pow.js SHA-256 << 24 符号溢出L107 逐字节 `<<24<<16
solveNonce 主线程阻塞L57 for(;i<1e7;) 同步 hash高难度/低端设备会卡 UI。建议 Web Worker 化(已列入后续优化)。 ⚠️ 待优化
index.php 会话正则 `user_id\i:`L43 `preg_match('/(?:^
index.php 重复 DB 连接L32-36 与 L73 两处 require config + SQLite 读前置缓存的绝大多数开销在磁盘;可把会话检查延迟到缓存 miss 后再执行。 ⚠️ 待优化


五、结论与下一步

  • 必须优先处理(建议先改):#1 语言切换 XSS、#3 RateLimiter 抢锁放行、#2 fetchImage SSL 取舍、#4 IP 来源。
  • 低成本加固(可顺带做):#6/#7 前端函数兜底与重复绑定、#8 登出转义、#13 Nginx index 兜底。
  • 审计误判:#18 邮箱 token 不一致(不成立)、fetchImage 多 IP 绕过(不成立)、login 锁并发覆盖(部分不成立)。
  • 架构/性能项(#5 GC、#9 视图 SQL、#11 pow 限流、#14 事务、#15 PRAGMA、pow.js Web Worker)可纳入后续迭代。

当前仅整理归档,未改动任何代码。请在 UI 按需下达修复指令(可逐条确认,也可按优先级批量处理)。

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

全部回复 (0)

暂无回复

请 登录