[审计]FlintHub app 目录 PHP 静态安全审计报告

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

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

FlintHub app 目录 PHP 静态安全审计报告

  • 审计日期:2026-09-09
  • 审计范围:d:\phpstudy_pro\WWW\app\ 目录下全部 146 个 PHP 文件(用户侧控制器 13、后台控制器 17、Core 6、SplitDB 14、Helpers 24、Models 10、Views 61、init.php 1)
  • 审计方式:全量静态审计(非 diff 审查),6 路并行逐文件通读 + 高危发现人工复核源码确认;只报告"入口 → 传播路径 → 危险汇"链路完整、无有效净化的可证实问题
  • 严重度定义:HIGH=可直接利用(RCE/鉴权绕过/大范围数据泄露);MEDIUM=特定但现实条件下可利用;LOW=纵深防御缺口或影响有限

一、总体结论

统计项数量
审计文件总数146
确认发现(置信度 ≥ 0.80)12(HIGH 2 / MEDIUM 3 / LOW 7)
低置信度提示(0.70–0.79,供人工评估)1
完全干净目录SplitDB(14/14)、Models(10/10)

总体评价:该代码库安全水位较高。SQL 全程 PDO 预处理、富文本清洗为强边界(未发现绕过)、CSRF 覆盖全部写操作、上传双白名单、IDOR 面逐一校验属主、后台双级鉴权。本轮发现集中在业务逻辑缺陷(积分刷取)、模板引擎防线的实现瑕疵(safeExpr 首匹配短路)与个别输出点转义姿势错误(后台标签管理 onclick 属性)。建议优先修复 H-1、H-2、M-1 三项。


二、防护基线确认(正面结论)

以下基线经逐文件核实,可作为后续开发的信任前提:

防护面结论依据位置
SQL 注入全程 PDO 预处理,EMULATE_PREPARES=false;Model 层 buildWhere 白名单 fail-closed、$fillable 列白名单、ORDER BY/LIMIT 强转;全部拼接点(IN/LIMIT/游标)均 int 强转或占位符化Core/Database.php、Core/Model.php
富文本 XSSContent::sanitizeHtml 采用标签+属性+协议三重白名单,双编码/嵌套实体/属性剥离 trick 均无法命中,未发现绕过Helpers/Content.php
CSRFrandom_bytes(32) + hash_equals + 登录/登出轮换,全部写动作显式校验Helpers/Csrf.php
会话与认证登录/自动登录 session_regenerate_id(true);DB 级用户名锁 5 次/15 分钟 + IP 限速;统一错误提示防枚举;RememberMe token 仅存 sha256、命中即轮换Controllers/AuthController.php、Helpers/RememberMe.php
文件上传扩展名白名单 × 内置 MIME 映射硬约束(即使后台把 php 加入白名单也会被 MIME 表拒绝)、finfo 失败全拒、随机文件名用户不可控、SVG 不放行Helpers/Upload.php
模板转义.html 主题模板 {$var} 默认 htmlspecialchars(ENT_QUOTES),仅 {$var:raw} 显式不转义;{php} 标签已移除Core/TemplateCompiler.php
框架层Router 无路由→类/方法注入;Autoloader 双分支 .. 过滤 + realpath 前缀绑定;队列 payload 全 JSON、全库无 unserialize;标识符全部类常量或锚定正则Core/Router.php、Core/Autoloader.php
后台鉴权BaseController 构造器统一 admin 会话 + 二次密码验证(30 分钟窗)双级守卫,17 个后台控制器全部写动作均落在守卫之后Controllers/Admin/BaseController.php


三、发现明细

严重度汇总表

#类别标题严重度置信度位置
H-1xss_stored后台标签管理 onclick 属性内 JS 字符串逃逸(存储型 XSS)HIGH0.88app/Views/admin/tags.php:39
H-2template_injection模板 safeExpr 仅校验首个函数调用,{if isset(...) && 恶意调用} 整体放行HIGH(有前提)0.78*app/Core/TemplateCompiler.php:159-165
M-1logic_flaw重复点赞重复发积分,双小号互刷可无限刷取积分/等级体系MEDIUM0.85app/Controllers/ApiController.php:81-87
M-2xss_storedbase_path 设置项零校验,写入全站 JS 字符串上下文(持久 XSS)+ 302 开放重定向MEDIUM0.85app/Controllers/Admin/SettingsController.php:24-27 → app/Views/layouts/main.php:66
M-3hardcoded_secretSMTP 加密密钥以默认值硬编码随源码分发MEDIUM0.90config.php:31
L-1authz/api/vote 不校验目标版块 canView,可对隐藏内容投票并形成存在性预言机LOW0.80app/Helpers/Vote.php:163-170
L-2authz/api/upload 不校验任何 can_attach 权限,绕过按版块上传策略LOW0.80app/Controllers/ApiController.php:126-149
L-3open_redirectlang 切换 PRG 回跳直接使用原始 REQUEST_URI,//evil.com 可跳外域LOW0.80app/init.php:121-131
L-4brute_force/admin/verify 二次验证无速率限制,会话被劫持后可高速爆破二次密码LOW0.85app/Controllers/Admin/VerifyController.php:18-28
L-5hardeningassets/uploads 位于 Web 根且无 .htaccess/web.config 禁执行文件(纵深缺失)LOW0.80config.php:22、Helpers/Upload.php:83-89
L-6audit_log_poison未认证请求可伪造任意受害者的 RememberMe 失配审计记录LOW0.80app/Helpers/RememberMe.php:38-41
L-7sensitive_exposureUser::findByUsername/findByEmail 返回含密码哈希/token 的全行LOW0.80app/Models/User.php:21-27

\* H-2 置信度 0.78 低于 0.80 上报门槛,但缺陷本身经直接代码复核客观确认(正则首匹配短路),唯一不确定项是利用前提(需模板文件写权限),故保留上报并注明前提。


H-1 后台标签管理存储型 XSS(HIGH,0.88)

位置:app/Views/admin/tags.php:39

输出点:$tag['name'] 经 $this->e()(htmlspecialchars)后输出到 onclick 属性内的单引号 JS 字符串:prompt(..., '<?php echo $this->e($tag['name']); ?>')。浏览器解析 HTML 属性时先实体解码再交给 JS 引擎,&#039; 还原为 ' 后即逃逸字符串。

源 → 汇:

普通用户发帖填写标签名
  → Tag::saveForThread()(app/Helpers/Tag.php:75-89,仅 trim + 长度 1~30,无字符白名单,原文入库)
  → tags 表 → TagController::index() → admin/tags.php:39 onclick 属性
  → 管理员点击该标签"重命名"按钮时 JS 执行

可利用性:任意注册用户创建名为 ');alert(document.cookie);//(≤30 字符)的标签;管理员打开后台标签页并点击该标签的重命名按钮即触发,脚本以管理员身份运行。需管理员一次交互,非纯被动。对比同文件第 42 行删除按钮已采用 htmlspecialchars(json_encode(...)) 的正确姿势,第 39 行为漏网之笔。

修复建议:改用第 42 行同款模式——用 htmlspecialchars(json_encode($tag['name']), ENT_QUOTES, 'UTF-8') 输出整个 JS 字符串实参;同时建议在 Tag::saveForThread 对标签名增加字符白名单(如仅允许字母数字/中文/-/_)。


H-2 模板表达式代码注入:safeExpr 首匹配短路(HIGH,有前提,0.78)

位置:app/Core/TemplateCompiler.php:159-165

缺陷:safeExpr() 的第二道防线用 preg_match('/(?:[a-zA-Z_][a-zA-Z0-9_]|\$\w+)\s\(/', ...) 只取第一个函数调用形态判断:若首个调用是 isset/empty,表达式其余部分不再检查即整体放行(L165 return $expr;)。第一道字符集白名单允许 ' " ( ) . , ! ? : | & = < >,足以构造任意无分号 PHP 表达式。

源 → 汇:

可写 .html 模板文件(主题目录 / 插件 views / app/Views)
  → compile() 的 {if}/{elseif} 捕获(TemplateCompiler.php:107-108, 148-149)
  → safeExpr 首匹配短路放行(:159-165)
  → 编译产物写 protected/cache/templates/tpl_*.php 并 require 执行(:236-247)
  → 任意代码执行(如 {if isset($x) && system('id')})

可利用性:需要攻击者已能写入 .html 模板文件——当前主题上传功能已禁用、未发现其他模板写入口,故单独不可直接利用;但该函数的存在意义正是防"主题上传场景"(与 L134 移除 {php} 同一威胁模型),本缺陷使该防线形同虚设。字符集还允许 $this->,可进一步调用编译器公共方法。

修复建议:将单次 preg_match 改为 preg_match_all 校验表达式内所有函数调用形态均属 isset/empty;或直接从白名单字符集中移除 ( ),为 isset/empty 提供专用语法糖。


M-1 重复点赞重复发积分(MEDIUM,0.85)

位置:app/Controllers/ApiController.php:81-87、app/Helpers/Vote.php:180-186

源 → 汇:

POST /api/vote vote=1(有登录+CSRF+30 次/分限流)
  → Vote::toggle UPSERT(Vote.php:180,ON CONFLICT 下重复点赞仍"成功",恒返回 user_vote=$vote)
  → ApiController.php:81 仅判 $vote==1 && user_vote==1 即 Points::award,无"首次点赞"状态迁移判定、无 (投票者,目标) 维度去重

可利用性:双小号互刷:点赞 → 取消 → 再点赞循环,每次都给帖主发分。限流只减速不去重(30 次/分),单目标约 1800 积分/小时/账号,且可批量操控多个目标内容。影响积分/等级体系完整性及等级关联权限。

修复建议:Vote::toggle 返回"是否由非赞态首次变为赞态"(对比 UPSERT 前后的实际行状态),仅状态迁移时发分;并对 (voter, target) 维度做积分发放去重。


M-2 base_path 设置项 → 全站持久 XSS + 开放重定向(MEDIUM,0.85)

位置:app/Controllers/Admin/SettingsController.php:24-27(存储,零校验)→ app/Views/layouts/main.php:66(JS 字符串汇,未转义)

源 → 汇:

POST /admin/settings/basic($_POST['base_path'] 直存,无格式校验)
  → init.php:160-163 define('BASE_PATH', $bp)
  → 汇① main.php:66  window.BASE_PATH='<?php echo BASE_PATH; ?>';   ← JS 单引号字符串可逃逸 → 全站每页持久 XSS
  → 汇② Core/Controller.php:101-104  Location: BASE_PATH.$url     ← 值为 //evil.com 形态时全站 302 跳外域

可利用性:需要有效管理员会话 + CSRF token。典型链路:攻击者仅劫持管理员会话(尚未通过二次验证)后,借同域任意注入点设置 base_path='';alert(document.cookie)//,获得全站持久 XSS,进而挂钩 /admin/verify 表单窃取二次验证密码,完成"会话 → 持久管理员"提权。

修复建议:服务端校验 base_path 必须匹配 ^\/[A-Za-z0-9_\-\/]*$(禁 //、引号、反斜杠);main.php 输出侧改用 htmlspecialchars(json_encode(BASE_PATH)) 或直接 json_encode 包裹。


M-3 SMTP 加密密钥硬编码随源码分发(MEDIUM,0.90)

位置:config.php:31 —— MAIL_PASS_KEY 以固定默认值写死在源码中。

源 → 汇:源码常量 → Mailer::decryptSmtpPass(app/Helpers/Mailer.php:78,90)→ 还原 settings 表中 SMTP 授权码明文。

可利用性:FlintHub 为分发型程序,所有未改密钥的部署共享同一默认密钥。任何能读到源码的途径(git 泄露、备份文件、未来任意文件读取类漏洞)即可解密数据库中的 SMTP 密码;加密仅防"只拿到数据库"的场景,源码与密钥同库存放使防线失效。

修复建议:密钥移出代码库(环境变量或 protected/ 下独立密钥文件,Web 不可达),安装时随机生成;发布升级公告引导存量站点轮换密钥并重加密存量 SMTP 密码。


L-1 /api/vote 不校验版块可见性(LOW,0.80)

位置:app/Helpers/Vote.php:163-170(经 ApiController.php:66-75 调用)

投票前仅校验目标存在于 topic_index/reply_index,未按目标 category_id 调用 Permission::canView,软删目标亦不排除。登录用户可对无权浏览版块的帖子/回复投票;"404 目标不存在 vs 成功"差异可枚举隐藏版块帖子 ID;叠加 M-1 可给隐藏内容刷积分。建议 toggle 前补 canView + status 校验。

L-2 /api/upload 绕过 can_attach 版块上传策略(LOW,0.80)

位置:app/Controllers/ApiController.php:126-149

该接口仅校验登录 + CSRF + 限流,完全不校验 can_attach。组权限被设为各版块 can_attach=0 的用户(发帖/回帖/编辑路径均被拦截)仍可经此直传白名单类型文件并获得站点外链,绕过按版块上传策略(含磁盘占用滥用)。建议纳入统一上传权限体系。

L-3 lang 切换回跳开放重定向(LOW,0.80)

位置:app/init.php:121-131

lang 白名单校验通过后,PRG 回跳的 Location 直接取原始 REQUEST_URI(仅剔除 lang 参数),未拒绝 // 开头。攻击链:诱导访问 https://站点//evil.com/?lang=zh-cn(同站路径形式链接,浏览器会原样发送 GET //evil.com/?lang=zh-cn 请求行)→ 回跳 Location: //evil.com/ → 浏览器跳转外域,可用于钓鱼跳板。建议回跳前仅取 parse_url(..., PHP_URL_PATH) 并拒绝 // 前缀。

L-4 /admin/verify 二次验证无限速(LOW,0.85)

位置:app/Controllers/Admin/VerifyController.php:18-28

仅 CSRF + password_verify,无 RateLimiter/锁定/PoW(对比登录有双层限速)。前提是攻击者已持有有效管理员会话(Cookie 窃取、M-2 场景),此时可高速爆破二次验证密码突破最后一道防线。建议复用 RateLimiter(如 5 次/15 分钟)并在连续失败后使会话失效。

L-5 uploads 目录禁执行防护依赖服务器配置(LOW,0.80)

位置:config.php:22、app/Helpers/Upload.php:83-89

assets/uploads/ 位于 Web 根下但目录内无 .htaccess/web.config(对照 Plugin::db 会为插件数据目录自动补写双防护)。当前白名单 + MIME 硬约束使 .php/.svg 无法落盘,不可直接利用;但禁执行完全依赖 nginx-server.conf 的 location 规则,迁移到 Apache/IIS 或配置漂移即洞。建议仿照 Plugin::db 在 uploads/ 及其子目录自动写入禁执行文件,使防护随应用走。

L-6 RememberMe 失配审计日志可被未认证伪造(LOW,0.80)

位置:app/Helpers/RememberMe.php:38-41

攻击者携带 remember_user_id=<任意受害者ID> + 任意假 token 的 cookie 访问任意页面,logMismatch 即以该受害者 ID 写入 remember_fail 审计记录——无需登录、无需 CSRF,可批量污染安全审计/风控取证视图(IP 取 REMOTE_ADDR,无法冒名,仅污染)。建议此类失配记录的 target_id 降为 0,不关联具体用户。

L-7 用户模型敏感字段过度返回(LOW,0.80)

位置:app/Models/User.php:21-27

findByUsername/findByEmail 返回 users 全行(含 password 哈希、email_verify_token、email、role、status),而 MessageController:117、AuthController:308/477 等调用方仅需 id/存在性。不构成直接可利用漏洞(未证实透出到响应),但违反最小取数原则,一旦后续误将整行透传(调试输出、JSON 返回)即泄露。建议改为显式列清单,登录链路单独保留含 password 的方法。


四、低置信度提示(0.70–0.79,供人工评估,不计入确认发现)

  1. Controller::redirect() 未拦截 /\ 协议相对变体(app/Core/Controller.php:97,置信 0.72):仅拒绝非 / 开头与 // 开头,Location: /\evil.com 在现代浏览器等价 //evil.com。当前调用方均传服务端构造路径,未见端到端可控入口;但该函数是公共汇点,建议统一改为"路径含反斜杠即拒绝"。

五、存疑与加固备注(不构成漏洞,供后续关注)

  • BaseController.php:56 功能缺陷(fail-closed,非安全洞):调用 Csrf::verifyOrDie() 但文件头未 use app\Helpers\Csrf,命名空间下解析为不存在的类 → 帖子/博客"批量转移版块"动作会 500。安全不受影响(校验 fail-closed),功能不可用,建议修复。
  • DatabaseController::mapBackupToTarget 前缀校验瑕疵(约 L1010-1015):strpos($realTarget, $realRoot) 未给根加尾分隔符,兄弟目录可前缀命中;利用需管理员 + 导入 CSRF + 目标目录已存在 + 限 .sqlite,危害边际。建议对齐 Plugin.php:376 的 rtrim(...).DIRECTORY_SEPARATOR 写法。
  • ExternStorage::read/delete 的 $relPath 汇点无路径校验:已核实当前全部 extern_path 写入点均为 ExternStorage::write() 内部生成、无外部入口;若未来出现导入/迁移功能将用户数据写入该列,即成任意文件读取/删除。建议对汇点加锚定正则。
  • admin/search_settings.php:43 $rebuildNext 未转义进 <meta http-equiv=refresh>,当前来源为服务端拼接(int + urlencode token)无用户可控链路,建议统一加 e()。
  • PostController.php:84 错误分支引用 L88 才定义的 $categoryId(未定义变量告警),功能缺陷非安全问题。
  • custom_css 过滤可绕过但无汇:ThemeController 仅精确替换 </style>,但全站无前台渲染点,当前不构成漏洞;若未来恢复渲染需改白名单式 CSS 净化。
  • Core/Model.php:219 sanitizeOrderBy 理论上可构造 ORDER BY 盲注,但当前调用方全部硬编码、无实际触达链路;若未来透传用户排序参数需先行加固。
  • CSP 含 'unsafe-inline' 'unsafe-eval'(Core/Controller.php:77),配置偏弱,具备条件时可收紧。

六、分目录审计结论

目录文件数结论
app/Controllers(用户侧)13 + init.phpApiController 3 项(M-1、L-1、L-2);init.php 1 项(L-3);其余 11 个控制器干净(HomeController、ForumController、BlogController、ThreadController、MessageController、ProfileController、SearchController、PostController、AuthController、ThemeController、AttachmentController、TagsController)
app/Controllers/Admin17SettingsController 1 项(M-2)、VerifyController 1 项(L-4)、BaseController 1 项存疑(fail-closed 功能缺陷);其余 14 个干净
app/Core6TemplateCompiler 1 项(H-2)、Controller 1 项低置信(/\ 变体);Router、Autoloader、Database、Model 干净
app/SplitDB14全部干净(CountingPDO、DBFactory、ShardRouter、Schema、IDGenerator、Queue、QueueConsumer、ViewCounter、SearchIndexStore、ExternStorage、ExternIdxRepairer、ExternMerger、ExternNamespaceMigrator、BucketAutoScaler)
app/Helpers24RememberMe 1 项(L-6);其余 23 个干净,其中 Content/Upload/Csrf/RememberMe/Auth 构成的安全基线质量高
app/Models10User 1 项(L-7);其余 9 个干净,全部 SQL 拼接点(IN/LIMIT/游标/常量)逐一核实安全
app/Views61admin/tags.php 1 项(H-1);其余 60 个干净(前台 31 个、后台 29 个),输出统一走 $this->e()


七、修复优先级建议

  1. 立即:H-1(一行级修复,改用同文件 L42 的 json_encode 模式);
  2. 本周:H-2(safeExpr 改 preg_match_all)、M-1(积分状态迁移判定 + 去重)、M-2(base_path 白名单 + 输出转义);
  3. 随版本:M-3(密钥外置 + 轮换)、L-1 ~ L-7 逐项;
  4. 顺手修复:BaseController 的 use 缺失(批量转移版块功能当前不可用)、PostController 未定义变量。

八、审计方法说明

  • 采用 6 路并行审计(用户侧控制器 / 后台控制器 / Core+SplitDB 框架层 / Helpers / Models / Views),每路逐文件全文通读、不抽样;框架基线先行(Database/TemplateCompiler/Csrf/Auth/Permission 实现要点先行确认)。
  • 每条发现均要求"入口 → 传播路径 → 危险汇"链路完整且中途无有效净化/授权;纯推测、DoS、代码风格、理论性缺陷不计入确认发现。
  • HIGH/MEDIUM 级发现已由主审逐一回读源码复核(tags.php:39、TemplateCompiler:159-165、ApiController:66-149、Vote:163-186、Tag:75-89、SettingsController:24-27、main.php:66、config.php:31、init.php:121-131、Controller.php:97-108、VerifyController:18-28),行号均为当前文件实际行号。
  • 排除项:DoS/资源耗尽、第三方依赖版本、纯文档、测试代码、ReDoS、无具体可达路径的 TOCTOU、仅日志回显类问题。
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 3 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录