本文由 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 |
| 富文本 XSS | Content::sanitizeHtml 采用标签+属性+协议三重白名单,双编码/嵌套实体/属性剥离 trick 均无法命中,未发现绕过 | Helpers/Content.php |
| CSRF | random_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-1 | xss_stored | 后台标签管理 onclick 属性内 JS 字符串逃逸(存储型 XSS) | HIGH | 0.88 | app/Views/admin/tags.php:39 |
| H-2 | template_injection | 模板 safeExpr 仅校验首个函数调用,{if isset(...) && 恶意调用} 整体放行 | HIGH(有前提) | 0.78* | app/Core/TemplateCompiler.php:159-165 |
| M-1 | logic_flaw | 重复点赞重复发积分,双小号互刷可无限刷取积分/等级体系 | MEDIUM | 0.85 | app/Controllers/ApiController.php:81-87 |
| M-2 | xss_stored | base_path 设置项零校验,写入全站 JS 字符串上下文(持久 XSS)+ 302 开放重定向 | MEDIUM | 0.85 | app/Controllers/Admin/SettingsController.php:24-27 → app/Views/layouts/main.php:66 |
| M-3 | hardcoded_secret | SMTP 加密密钥以默认值硬编码随源码分发 | MEDIUM | 0.90 | config.php:31 |
| L-1 | authz | /api/vote 不校验目标版块 canView,可对隐藏内容投票并形成存在性预言机 | LOW | 0.80 | app/Helpers/Vote.php:163-170 |
| L-2 | authz | /api/upload 不校验任何 can_attach 权限,绕过按版块上传策略 | LOW | 0.80 | app/Controllers/ApiController.php:126-149 |
| L-3 | open_redirect | lang 切换 PRG 回跳直接使用原始 REQUEST_URI,//evil.com 可跳外域 | LOW | 0.80 | app/init.php:121-131 |
| L-4 | brute_force | /admin/verify 二次验证无速率限制,会话被劫持后可高速爆破二次密码 | LOW | 0.85 | app/Controllers/Admin/VerifyController.php:18-28 |
| L-5 | hardening | assets/uploads 位于 Web 根且无 .htaccess/web.config 禁执行文件(纵深缺失) | LOW | 0.80 | config.php:22、Helpers/Upload.php:83-89 |
| L-6 | audit_log_poison | 未认证请求可伪造任意受害者的 RememberMe 失配审计记录 | LOW | 0.80 | app/Helpers/RememberMe.php:38-41 |
| L-7 | sensitive_exposure | User::findByUsername/findByEmail 返回含密码哈希/token 的全行 | LOW | 0.80 | app/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 引擎,' 还原为 ' 后即逃逸字符串。
源 → 汇:
普通用户发帖填写标签名
→ 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)
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)
findByUsername/findByEmail 返回 users 全行(含 password 哈希、email_verify_token、email、role、status),而 MessageController:117、AuthController:308/477 等调用方仅需 id/存在性。不构成直接可利用漏洞(未证实透出到响应),但违反最小取数原则,一旦后续误将整行透传(调试输出、JSON 返回)即泄露。建议改为显式列清单,登录链路单独保留含 password 的方法。
四、低置信度提示(0.70–0.79,供人工评估,不计入确认发现)
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.php | ApiController 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/Admin | 17 | SettingsController 1 项(M-2)、VerifyController 1 项(L-4)、BaseController 1 项存疑(fail-closed 功能缺陷);其余 14 个干净 |
| app/Core | 6 | TemplateCompiler 1 项(H-2)、Controller 1 项低置信(/\ 变体);Router、Autoloader、Database、Model 干净 |
| app/SplitDB | 14 | 全部干净(CountingPDO、DBFactory、ShardRouter、Schema、IDGenerator、Queue、QueueConsumer、ViewCounter、SearchIndexStore、ExternStorage、ExternIdxRepairer、ExternMerger、ExternNamespaceMigrator、BucketAutoScaler) |
| app/Helpers | 24 | RememberMe 1 项(L-6);其余 23 个干净,其中 Content/Upload/Csrf/RememberMe/Auth 构成的安全基线质量高 |
| app/Models | 10 | User 1 项(L-7);其余 9 个干净,全部 SQL 拼接点(IN/LIMIT/游标/常量)逐一核实安全 |
| app/Views | 61 | admin/tags.php 1 项(H-1);其余 60 个干净(前台 31 个、后台 29 个),输出统一走 $this->e() |
七、修复优先级建议
- 立即:H-1(一行级修复,改用同文件 L42 的 json_encode 模式);
- 本周:H-2(safeExpr 改 preg_match_all)、M-1(积分状态迁移判定 + 去重)、M-2(base_path 白名单 + 输出转义);
- 随版本:M-3(密钥外置 + 轮换)、L-1 ~ L-7 逐项;
- 顺手修复: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、仅日志回显类问题。