[审计]FlintHub 静态审计报告(mimo-v2.5-pro)

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

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

FlintHub 静态审计报告(mimo-v2.5-pro)

审计范围:app/ 目录下全部 145 个 PHP 文件

审计方式:纯静态分析(AST 模式匹配 + 人工代码审查),不修改任何代码

审计日期:2026-09-09

审计模型:AtomCode (mimo-v2.5-pro)


Top 10 安全发现

P0 — 必须立即修复(可直接导致安全事件)


#1 SSRF:IPv6 私有地址绕过 IP 黑名单

项目内容
文件app/Controllers/ApiController.php
行号L276-279(isBlockedIp)+ L267-271(isBlockedHost)
严重程度P0

问题描述:isBlockedIp() 和 isBlockedHost() 仅拦截 IPv4 私有段(10.x / 172.16-31.x / 192.168.x / 127.x 等)和 ::1,但 完全缺失 以下 IPv6 黑名单:

  • IPv6 ULA(fd00::/8):等价于 IPv4 的 10.0.0.0/8,局域网内部地址
  • IPv6 Link-Local(fe80::/10):链路本地地址,可访问宿主机服务
  • IPv4-Mapped IPv6(::ffff:10.0.0.1):PHP 流/CURL 会将其连接到 10.0.0.1,正则 ^(10\.) 无法匹配 ::ffff:10.x.x.x
  • IPv6 环回(0:0:0:0:0:0:0:1 的全写形式):仅匹配缩写 ::1

严重后果:攻击者注册一个域名,将其 AAAA 记录指向 fd00::1(内网网关)或 ::ffff:10.0.0.1,然后通过 GET /api/fetch-image?url=http://attacker-domain/ 即可 穿透 SSRF 防护,访问内网任意服务(数据库管理面板、云元数据 169.254.169.254 的 IPv6 等价物、内部 API)。结合 #2 的 CSRF 问题,未登录攻击者也能借已登录用户之手发起请求。


#2 SSRF 端点使用 GET 方法 — 无 CSRF 防护

项目内容
文件app/Controllers/ApiController.php
行号L166-174
严重程度P0

问题描述:fetchImage() 显式使用 GET 方法接收外部 URL,且无 CSRF token 校验:

// L174: 接受 GET 请求
if ($_SERVER['REQUEST_METHOD'] !== 'GET' || empty($_GET['url'])) {
    $this->jsonError('Invalid request');
}
// L170: 仅检查登录态,无 CSRF
if (!isset($_SESSION['user_id'])) { ... }

严重后果:攻击者可在任意网站嵌入:


已登录用户的浏览器会自动携带 Cookie 发起请求,服务端在用户上下文中执行 SSRF。不需要受害者点击任何东西,仅需访问攻击者页面。与 #1 的 IPv6 绕过组合后,构成完整的 SSRF 攻击链。


P1 — 高危(需要尽快修复)


#3 ZIP 解压未检测符号链接 — 任意文件覆写

项目内容
文件app/Controllers/Admin/DatabaseController.php
行号L776-786(extractZipToData)
严重程度P1

问题描述:ZIP 路径穿越检查(.. / 绝对路径 / 盘符)正确,但 未检测 ZIP 条目是否为符号链接(symlink):

// L776-782: 仅检查路径字符串,未检查条目类型
for ($i = 0; $i < $zip->numFiles; $i++) {
    $name = str_replace('\\', '/', (string)$zip->getNameIndex($i));
    if ($name === '' || $name[0] === '/' || strpos($name, '..') !== false ...) { ... }
}
// L786: 直接解压,未排除 symlink 条目
if (!$zip->extractTo($root)) { ... }

严重后果:攻击者(需管理员权限)构造恶意 ZIP,内含 symlink 条目指向 ../../config.php 或 ../../index.php。解压时 ZipArchive::extractTo 会跟随符号链接写入目标文件,覆盖站点核心配置或植入后门。虽然需要管理员权限,但管理员账户被钓鱼/撞库后此漏洞可直接 RCE。


#4 后台 base_path 设置未校验 — URL 注入

项目内容
文件app/Controllers/Admin/SettingsController.php
行号L24-27
严重程度P1

问题描述:base_path 从 $_POST 直接写入数据库,无任何格式校验:

// L24-27: 直接存储用户输入
$keys = ['site_name', 'site_description', 'base_path', 'site_lang'];
foreach ($keys as $key) {
    $settingModel->setValue($key, $_POST[$key] ?? '');
}

base_path 用于全局 URL 生成($this->url() / $this->asset()),在 TemplateCompiler.php L359-364 中拼入 <a href> / <link href> / <script src>。

严重后果:若 base_path 被设为 javascript:alert(1)// 或 //evil.com,则站点所有链接、CSS、JS 引用均被劫持。虽然需要管理员权限,但一旦被利用:① 可构造存储型 XSS(通过 javascript: 协议);② 可劫持所有静态资源到恶意 CDN(通过 //evil.com 前缀);③ 管理员可能被社工诱导修改此值。


#5 模板引擎 {!! !!} 原样输出 — 存储型 XSS 风险面

项目内容
文件app/Core/TemplateCompiler.php
行号L145
严重程度P1

问题描述:模板编译器提供 {!! $var !!} 语法(cbRaw),直接输出变量值,不经过 htmlspecialchars:

// L145: 原样输出,零转义
protected function cbRaw($m) { return '<?php echo ' . $this->fixArrayKeys($m[1]) . ' ?? \'\'; ?>'; }

对比安全版本:

// L144: 自动 htmlspecialchars 转义
protected function cbVar($m) { return '<?php echo htmlspecialchars(' . $this->fixArrayKeys($m[1]) . ' ?? \'\', ENT_QUOTES, \'UTF-8\'); ?>'; }

严重后果:当前视图中未发现对用户输入使用 {!! !!} 的情况(已逐一检查),但此语法作为"逃生舱口"存在,未来任何开发者误用 {!! $post.content !!} 即可引入存储型 XSS。框架层面无任何编译期警告或运行时检测机制。建议:在 cbRaw 中增加 error_log 警告,或对包含 $_POST / $_GET / 用户来源变量的 {!! !!} 表达式编译时报错。


#6 后台 SQLite 导入仅校验文件头 — 可植入恶意数据库

项目内容
文件app/Controllers/Admin/DatabaseController.php
行号L887-893(handleImport)
严重程度P1

问题描述:.sqlite 文件导入仅校验文件头 16 字节是否为 SQLite format 3,之后直接 copy 到目标路径:

// L888-893: 仅校验魔术字节
$head = @file_get_contents($uploaded, false, null, 0, 16);
if ($head === false || strpos($head, 'SQLite format 3') !== 0) {
    return ...; // 拒绝
}
if (!@copy($uploaded, $target)) { ... } // 直接覆盖

严重后果:攻击者构造一个合法的 SQLite 文件,其中 users 表包含预置的管理员账户(role=admin, 已知密码哈希),或在 settings 表中插入恶意 base_path,或在 SQLite 触发器中植入恶意逻辑。导入后攻击者直接获得管理员权限。应校验数据库内容完整性(如检查 users 表结构/记录数是否异常、是否存在未知触发器等)。


#7 后台恢复操作通过 GET 触发状态变更 — 绕过 CSRF

项目内容
文件app/Controllers/Admin/MaintenanceController.php
行号L58-79
严重程度P1

问题描述:runtime_rebuild 操作分两步执行:POST 请求写入标记文件,然后重定向到 GET 请求执行实际操作。GET 分支不校验 CSRF:

// L58-62: POST 写标记(有 CSRF)
if ($action === 'runtime_rebuild') {
    Csrf::verifyOrDie($_POST['csrf'] ?? '');
    file_put_contents($pendingFile, time());
    $this->redirect('/admin/maintenance?rebuild_pending=1');
}
// L65-78: GET 执行(无 CSRF,注释明确说"移除 CSRF 校验")
if (isset($_GET['rebuild_pending']) && file_exists($pendingFile)) {
    // 移除 CSRF 校验:此分支由 runtime_rebuild POST 重定向触发
    ... // 重建计数器
}

严重后果:攻击者可通过 CSRF 诱导管理员发起 POST 请求(已有 CSRF 防护所以这步被拦),但如果攻击者能预测/暴力 $pendingFile 路径(protected/.rebuild_pending,固定路径),可直接构造 GET URL 发给管理员触发。更关键的是,这个模式建立了错误的安全范式——"POST 有 CSRF 就行,GET 可以不做"——如果未来有人复制此模式到更敏感的操作,后果严重。


P2 — 中危(建议修复)


#8 safeReferer 允许 localhost 作为合法跳转目标

项目内容
文件app/Controllers/ApiController.php
行号L650-661
严重程度P2

问题描述:safeReferer() 白名单中硬编码允许 localhost 和 127.0.0.1:

// L657: localhost 和 127.0.0.1 被视为合法来源
if ($host === null || $host === '' || $host === $siteHost
    || $host === 'localhost' || $host === '127.0.0.1') {
    return $referer;
}

严重后果:在同机部署场景(如开发环境、反向代理同源部署),攻击者从 http://localhost:恶意端口/ 发起请求时,safeReferer 会返回该 URL 作为重定向目标。虽然影响场景有限(需同源/同机),但在 Location 头中注入 localhost URL 可能被用于开放重定向链的跳板。


#9 SSRF fetchHopByHop 禁用 SSL 证书验证

项目内容
文件app/Controllers/ApiController.php
行号L306-309(fopen 分支)+ L347-348(cURL 分支)
严重程度P2

问题描述:远程图片拉取的两个 HTTP 客户端均禁用 SSL 验证:

// L306-309: fopen 分支
'ssl' => [
    'verify_peer' => false,
    'verify_peer_name' => false,
],
// L347-348: cURL 降级分支
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => 0,

注释说明"连接 IP 而非域名,证书无法匹配,故跳过验证"。这是 SSRF 防护的副作用(IP 替换 host 导致证书域名不匹配)。

严重后果:中间人攻击者可篡改远程图片响应,注入恶意内容(如将图片替换为含 EXIF XSS 的文件)。虽然影响限于图片内容(后续有 MIME 校验),但在特定场景下(如 Imagick 历史漏洞 CVE-2016-3714)可能升级为 RCE。建议:对 HTTPS 请求恢复 host 名称做 SNI + 证书校验。


#10 上传扩展名白名单由管理员配置 — 可开放危险类型

项目内容
文件app/Helpers/Upload.php
行号L25-28
严重程度P2

问题描述:上传文件的扩展名白名单从数据库设置读取,管理员可在后台自定义:

// L25-28: 从 Settings 读取,管理员可修改
$extList = Settings::get('attachment_allowed_ext', implode(',', ALLOWED_EXTENSIONS));
$allowedTypes = array_filter(array_map('trim', explode(',', $extList)));

虽然有 MIME 校验(L61-73 的 $allowedMimes 映射表),但 $allowedMimes 映射表是硬编码的,不在其中的扩展名会被拒绝。如果管理员在白名单中加入 php,MIME 校验会因 php 不在 $allowedMimes 中而拒绝。所以当前有纵深防御。

严重后果:当前安全(MIME 校验兜底),但如果未来有人扩展 $allowedMimes 映射表加入 php => ['text/x-php'](比如为了支持插件上传),则白名单 + MIME 双重防线同时被突破。建议:在 $allowedTypes 读取后增加危险扩展名黑名单过滤(php/phtml/phar/asp/aspx/jsp 等无论白名单如何配置均拒绝)。


审计总结

等级数量说明
P02SSRF IPv6 绕过 + GET 端点无 CSRF,组合可形成完整攻击链
P15ZIP 符号链接、base_path 注入、模板 raw 输出、SQLite 导入、GET 状态变更
P23localhost 开放重定向、SSL 验证禁用、上传扩展名配置风险

正面发现(做得好的地方)

  • SQL 注入防护扎实:Model 层全面使用参数化查询,buildWhere() 有严格的白名单解析器拒绝 OR/子查询/字面量,insert()/update() 有列名正则校验 + fillable 白名单
  • 无 eval/assert/exec 命令注入:代码中无 eval()、assert(),exec() 仅用于 PDO::exec() 的 SQLite PRAGMA 操作
  • 无 unserialize 风险:RateLimiter 已从 unserialize 迁移到 json_decode
  • CSRF 防护全面:所有后台 POST 操作均调用 Csrf::verifyOrDie(),前台关键操作也有 CSRF 校验
  • 文件上传安全:Upload.php 有扩展名白名单 + MIME 校验 + 随机文件名 + null byte 检测 + 像素炸弹防护
  • 路径遍历防护到位:数据库下载/删除使用 basename() + realpath() 双重校验,ZIP 解压有 .. / 绝对路径检测
  • RememberMe 安全:使用 hash_equals 做 token 比较,SHA-256 哈希存储,多端独立 token
  • 模板变量默认转义:{var} 语法自动 htmlspecialchars,仅 {!! !!} 需显式选择不转义

不输出的"理论上应该重构"建议

按要求已忽略所有"应该重构"类建议,仅输出可直接导致安全后果的具体问题。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 瀏覽 4 次 | 回覆 0 次

全部回覆 (0)

暫無回覆

請 登入