
Security Advisory: Unauthenticated Path Traversal Allows Arbitrary File Read (TinyWeb)
Identifiant CVE assigné : CVE-2026-67185
TinyWeb construit le chemin du fichier à servir en concaténant la racine web avec l'URL de la requête telle qu'elle arrive, sans aucune normalisation. Un client distant qui ne s'est jamais authentifié peut insérer des séquences ../ dans la ligne de requête et lire tout fichier que le processus serveur peut ouvrir, y compris des fichiers situés hors de la racine web.
TnyWeb/0.0.80b3b5fd (2019-01-09), où le chemin est assemblé de cette façon pour la première fois, jusqu'à a381da2 (2023-11-22, le plus récent sur master).CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:NUn attaquant distant non authentifié pouvant atteindre le port TCP d'écoute du serveur (9090 dans la configuration fournie). Aucun identifiant, aucune interaction utilisateur et aucun accès préalable ne sont requis. Une seule requête suffit.
HttpBuilder::buildResponse() commence par la racine web configurée et ajoute le champ de chemin de l'URL directement issu de la requête analysée :
// src/tiny_http/http_responser.cc:80
sdsncat(&file_path, server.www.c_str(), server.www.size());
// src/tiny_http/http_responser.cc:83-87
if (url->field_set & (1 << HTTP_UF_PATH)) {
unsigned int off = url->fields[HTTP_UF_PATH].offset;
unsigned int len = url->fields[HTTP_UF_PATH].len;
sdsncat(&file_path, url->data + off, len);
}
// src/tiny_http/http_responser.cc:90-94
std::string f(file_path.data, file_path.len);
file_type = isRegularFile(f);
if (0 == file_type) {
return_val = file->setFile(f); // stat() + open() on the joined path
}
Entre la concaténation à la ligne 86 et l'appel au système de fichiers à la ligne 94, il n'y a ni suppression des segments « . » (RFC 3986, section 5.2.4), ni canonisation par realpath(), ni vérification que le résultat reste dans server.www.
L'analyseur d'URL ne filtre pas non plus ... Dans HttpParser::parseUrlChar(), l'état du chemin accepte tout caractère qui satisfait isUrlChar(), et . est un caractère d'URL valide, donc ../ parvient à buildResponse() sans modification :
// src/tiny_http/http_parser.cc:653-654
case s_requ_path: //finished
if (isUrlChar(ch))
return s_requ_path;
HttpFile::setFile() ouvre ensuite et sert le chemin produit, quel qu'il soit, avec les privilèges du processus serveur :
// src/tiny_http/http_model_file.cc:43
return_val = open(name.c_str(), O_RDONLY);
Remarque : TinyWeb ne procède jamais au décodage percent du chemin d'URL, donc %2e%2e%2f ne fonctionne pas. La forme littérale ../, en revanche, fonctionne.
Démarrez le serveur avec la configuration fournie (écoute sur le port 9090 et sert les fichiers depuis la racine web configurée).
Envoyez une requête dont le chemin remonte au-dessus de la racine web. L'option --path-as-is empêche curl de réduire les ../ avant l'envoi :
curl --path-as-is 'http://TARGET:9090/../../../../etc/passwd'
200 OK avec le contenu de /etc/passwd :HTTP/1.1 200 OK
Server: TinyWeb/0.0.8
Transfer-Encoding: chunked
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
Le nombre de segments ../ nécessaires dépend de la profondeur à laquelle se trouve la racine web. Tout nombre égal ou supérieur à cette profondeur fonctionne, car le noyau ignore .. à la racine du système de fichiers.
Tout client distant peut lire tout fichier que le processus serveur est autorisé à ouvrir. Dans la configuration par défaut, les workers s'exécutent en root, ce qui inclut donc /etc/shadow, les clés privées TLS, les fichiers de configuration contenant des identifiants, ainsi que le code source de l'application lui-même sous la racine web.
Canonisez le chemin concaténé avec realpath() et rejetez la requête si le résultat ne se trouve pas dans la racine web configurée. Par défense en profondeur, supprimez les segments « . » lors de l'analyse de l'URL conformément à la section 5.2.4 de la RFC 3986, et décodez les pourcentages du chemin avant d'exécuter cette normalisation.