
Proof-of-Concept für CVE-2026-39376, das eine Denial-of-Service-Schleife durch unendliche Weiterleitungen über Meta-Refresh-Ketten in fastfeedparser demonstriert, mit Potenzial zur SSRF-Verkettung.
CVE-2026-39376 (Unendliche Weiterleitungsschleife als DoS über Meta-Refresh-Kette)
Wenn parse() eine URL abruft, die eine HTML-Seite mit einem <meta http-equiv="refresh">-Tag zurückgibt, ruft es sich rekursiv selbst mit der Weiterleitungs-URL auf – ohne Tiefenbegrenzung, ohne Deduplizierung bereits besuchter URLs und ohne Begrenzung der Weiterleitungsanzahl. Ein vom Angreifer kontrollierter Server, der eine unendliche Kette von HTML-Meta-Refresh-Antworten zurückgibt, verursacht eine unbegrenzte Rekursion, erschöpft den Python-Call-Stack und bringt den Prozess zum Absturz. Diese Schwachstelle kann auch mit dem begleitenden SSRF-Problem verkettet werden, um nach Umgehung der anfänglichen URL-Prüfung interne Netzwerkziele zu erreichen.
parse() fängt ValueError bei XML-Parse-Fehlern ab, extrahiert eine Meta-Refresh-URL aus der HTML-Antwort über _extract_meta_refresh_url() und ruft sich selbst mit dieser URL als Endrekursion auf. Der rekursive Aufruf ist bedingungslos – es gibt keine maximale Weiterleitungstiefe, keine Menge bereits besuchter URLs und keinen Schutz gegen selbstreferenzielle oder zyklische Weiterleitungen.
fastfeedparser/main.py – parse() (rekursive Senke):
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, ...) # ← bedingungslose Rekursion, keine Tiefenbegrenzung
_extract_meta_refresh_url() verwendet urljoin(base_url, match.group(1)), sodass relative, protokollrelative (//host/path) und absolute URLs im content=-Attribut alle verfolgt werden.
Kein Live-Server erforderlich. Das Folgende patcht _fetch_url_content per Monkeypatch, um eine unendliche HTML-Meta-Refresh-Kette zurückzugeben, und bestätigt die unbegrenzte Rekursion:
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}")
# Ausgabe:
# CONFIRMED infinite loop: Stopped at call 11
# Total fetches before stop: 11
Jeder rekursive Aufruf führt eine echte HTTP-Anfrage (30 s Timeout), HTML-Parsing und eine Python-Stack-Frame-Zuweisung durch. Mit Pythons Standard-Rekursionslimit von 1000 und einem 30-s-Timeout pro Anfrage kann eine einzelne Angreiferanfrage einen Server-Thread bis zu ~8 Stunden beschäftigen, bevor ein RecursionError ausgelöst wird.
SSRF-Kettenvariante: Die erste Antwort kann legitimes HTML sein, das auf eine interne Adresse (http://192.168.1.1/) weiterleitet, sodass die Weiterleitungsschleife auch als SSRF-Umgehung für Ziele dient, die andernfalls durch eine anwendungsseitige URL-Validierung blockiert würden, die nur auf die anfängliche URL angewendet wird.
Dies ist eine Denial-of-Service-Schwachstelle mit sekundärer SSRF-Verkettungsauswirkung. Jede Anwendung, die benutzerbereitgestellte Feed-URLs akzeptiert und fastfeedparser.parse() aufruft, ist betroffen – einschließlich RSS-Aggregatoren, Feed-Vorschaudiensten und „Per URL abonnieren“-Funktionen. Ein Angreifer ohne Authentifizierung kann:
RecursionError nach ~1000 Weiterleitungen zum Absturz bringen