
Aviso de Seguridad: Contrabando de Peticiones HTTP mediante Valores Transfer-Encoding no Analizados (tiny_http)
ID de CVE asignado: CVE-2026-66752
tiny_http solo comprueba si hay una cabecera Transfer-Encoding. Nunca examina
su valor. Cualquier valor, incluidos codings que no son chunked y listas de
codings cuyo elemento final no es chunked, hace que el cuerpo se decodifique
por chunks, y Content-Length se descarta al mismo tiempo.
Un front end que analiza el transfer coding correctamente enmarcará dicha petición de forma distinta a tiny_http. Dos participantes con dos enmarcados en una misma conexión es la condición previa para el contrabando de peticiones. Enviar un cuerpo no fragmentado con un coding no fragmentado también hace que tiny_http falle al leer el cuerpo y no responda nada.
URL del repositorio: https://github.com/tiny-http/tiny-http
| Afectadas | todas las versiones publicadas hasta 0.12.0 inclusive (2022-10-06), la versión actual |
| Verificadas | 0.6.2, 0.6.3, 0.8.0, 0.9.0, 0.10.0, 0.11.0, 0.12.0 |
| Corregidas en | ninguna versión corregida al momento de escribir esto |
CWE-444 (Interpretación inconsistente de peticiones HTTP).
Puntuación base CVSS 4.0 6.3 (Media)
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
Un cliente remoto no autenticado que envía HTTP sin procesar. Sin credenciales ni interacción del usuario.
El caso de contrabando requiere que tiny_http esté detrás de un front end o CDN
que reenvíe Transfer-Encoding y Content-Length y aplique la sección 6.1 del
RFC 9112 correctamente, es decir, que trate una lista de codings cuyo elemento
final no es chunked como no fragmentada y recurra a Content-Length. Ese front
end y tiny_http discrepan entonces sobre dónde termina el cuerpo.
El caso de petición sin respuesta solo necesita poder conectarse.
tiny_http-0.12.0/src/request.rs, líneas 143 a 153. La cabecera se localiza y
se clona, pero el valor solo se comprueba por presencia:
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, líneas 218 a 221, aplica después el
decodificador de chunks de forma incondicional:
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 es un Option<AsciiString> que contiene el valor sin
procesar, y nada inspecciona jamás su contenido. La sección 6.1 del RFC 9112
exige que el coding final de una petición sea chunked y que el servidor
rechace el mensaje en caso contrario, y la sección 6.3 exige rechazar una
petición que lleve tanto Transfer-Encoding como Content-Length.
Paso 1. Inicie un servidor que informe de las cabeceras de enmarcado y del cuerpo que leyó.
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"));
}
}
Paso 2. Envíe una petición con Transfer-Encoding: identity, una
Content-Length coincidente y un cuerpo normal. Cualquier front end enmarcaría
este cuerpo como los 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. tiny_http decodificó por chunks un cuerpo que no está fragmentado, lo descartó y no envió ninguna respuesta. El cliente espera hasta su propio tiempo de espera y la conexión se consume.
TE=Some("identity") CL=Some("5") read=Err(Custom { kind: InvalidInput, error: DecoderError }) body=""
Paso 3. Envíe una lista de codings cuyo elemento final no es 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. tiny_http la acepta y la decodifica como fragmentada, cuando la sección 6.1 del RFC 9112 exige el rechazo:
TE=Some("chunked, identity") CL=None read=Ok(5) body="hello"
Para contrastar, solo Content-Length y un Transfer-Encoding: chunked
correcto se comportan adecuadamente y producen body="hello".
Cuando tiny_http está detrás de un front end que analiza los transfer codings correctamente, los dos componentes discrepan sobre dónde termina el cuerpo de la petición. El atacante elige los bytes a cada lado de esa discrepancia, que es la configuración estándar para pasar de contrabando una petición más allá del enrutamiento y el control de acceso del front end.
Independientemente de cualquier front end, los pasos 2 y 3 muestran una petición trivialmente malformada que bloquea un worker y no recibe respuesta alguna, por lo que el cliente no puede saber que la petición fue rechazada.
Analice Transfer-Encoding como la lista separada por comas que es, en
src/request.rs alrededor de la línea 144:
Exija que el coding final sea chunked para una petición que lleve cuerpo, y
rechace cualquier otro coding con 400 en lugar de decodificarlo por chunks.
Rechace una petición que lleve tanto Transfer-Encoding como Content-Length,
como exige la sección 6.3 del RFC 9112 para un servidor que no es un proxy, en
lugar de ignorar silenciosamente Content-Length en la línea 150.
Cuando el lector del cuerpo falle, envíe un 400 en lugar de descartar la petición sin respuesta.