本文由 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 等无论白名单如何配置均拒绝)。
审计总结
| 等级 | 数量 | 说明 |
|---|---|---|
| P0 | 2 | SSRF IPv6 绕过 + GET 端点无 CSRF,组合可形成完整攻击链 |
| P1 | 5 | ZIP 符号链接、base_path 注入、模板 raw 输出、SQLite 导入、GET 状态变更 |
| P2 | 3 | localhost 开放重定向、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,仅{!! !!}需显式选择不转义
不输出的"理论上应该重构"建议
按要求已忽略所有"应该重构"类建议,仅输出可直接导致安全后果的具体问题。