Vulnerability GHSA-4j63-6c4x-wfg4

Medium Risk
MEDIUM RISK
CVSS Score: 6.0
Score Range: 4.0–6.9
Medium severity vulnerabilities (CVSS 4.0–6.9). Important issues that meaningfully reduce security confidence.
15 hours ago
November 13, 2026 at 12:45 AM UTC
Soup Sieve: General sibling combinator inside :has() matches untrusted HTML in quadratic time
0.4 and 0.5 and 0.5.1 - 0.5.3 and 0.6 and 1.0b1 and 1.0b2 and 1.0 and 1.0.1 - 1.0.2 and 1.1 and 1.2 and 1.2.1 and 1.3 and 1.3.1 and 1.4 and 1.5 and 1.6 and 1.6.1 - 1.6.2 and 1.7 and 1.7.1 - 1.7.3 and 1.8 and 1.9 and 1.9.1 - 1.9.6 and 2.0 and 2.0.1 and 2.1 and 2.2 and 2.2.1 and 2.3 and 2.3.1 - 2.3.2 and 2.3.2.post1 and 2.4 and 2.4.1 and 2.5 and 2.6 and 2.7 and 2.8 and 2.8.1 - 2.8.4 and 2.9 and 2.9.1 - 2.9.2
0.4 and 0.5 and 0.5.1 - 0.5.3 and 0.6 and 1.0b1 and 1.0b2 and 1.0 and 1.0.1 - 1.0.2 and 1.1 and 1.2 and 1.2.1 and 1.3 and 1.3.1 and 1.4 and 1.5 and 1.6 and 1.6.1 - 1.6.2 and 1.7 and 1.7.1 - 1.7.3 and 1.8 and 1.9 and 1.9.1 - 1.9.6 and 2.0 and 2.0.1 and 2.1 and 2.2 and 2.2.1 and 2.3 and 2.3.1 - 2.3.2 and 2.3.2.post1 and 2.4 and 2.4.1 and 2.5 and 2.6 and 2.7 and 2.8 and 2.8.1 - 2.8.4 and 2.9 and 2.9.1 - 2.9.2

Summary

Soup Sieve: General sibling combinator inside :has() matches untrusted HTML in quadratic time

Details

Summary

Matching a selector that puts a general-sibling combinator inside :has(), such as div:has(a ~ b), against untrusted HTML runs in O(n²) time in the number of sibling elements. A single small document with a few thousand siblings ties the process up for a minute of CPU, so an application that runs a fixed selector of this shape over HTML it doesn't control can be stalled by one request.

:has is powerful, but much in the same way poor regular expressions can be crafted to cause catastrophic backtracking with greedy capturing, has can be used to create non-ideal scenarios that can cause large portions of the tree to be evaluated for one or many elements if not used thoughtfully.

This behavior is not exclusive to Soup Sieve, but browsers make note of these potential performance issues themselves: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:has#performance_considerations.

With all of that said, there is room for improvements that can in some cases greatly improve performance.

Details

:has(a ~ b) asks "does this element contain an a that has a later sibling b?". When no b is present, soupsieve can't short-circuit, and it answers the question with a nested scan: match_future_child walks every candidate a under the scope, and for each one the general-sibling branch of match_future_relations walks all of that a's following siblings looking for b. With n siblings that is (n-1) + (n-2) + ... + 1 comparisons, i.e. O(n²).

The adjacent-sibling form div:has(a + b) stays linear, because its branch checks only the one immediately following sibling per candidate. So the cost is specific to the general-sibling ~ (and ~ nested inside :has), and it shows up on the common case where the right-hand side simply isn't there.

Proof of concept

Installed from PyPI: pip install soupsieve==2.9.2 beautifulsoup4.

import time
import soupsieve
from bs4 import BeautifulSoup

selector = "div:has(a ~ b)"

for n in (1000, 2000, 4000, 8000):
    html = "<div>" + ("<a></a>" * n) + "</div>"
    soup = BeautifulSoup(html, "html.parser")
    start = time.perf_counter()
    soupsieve.select(selector, soup)
    print(n, round((time.perf_counter() - start) * 1000), "ms")

Measured:

n=1000    837 ms
n=2000   3667 ms
n=4000  14797 ms
n=8000  59746 ms

Time quadruples for each doubling of n. The n=8000 input is a ~56 KB HTML fragment.

Impact

Availability. Any caller that evaluates a fixed selector containing a general sibling under :has() over attacker-controlled markup can be forced to spend minutes of CPU on a small input. The selector is developer-authored and ordinary; the untrusted part is the HTML, which is soupsieve's normal input.

A single-pass evaluation of the sibling run (remember whether an a has been seen while scanning the siblings once) would make this case linear.

Mitigation

Quadratic time for :has in some circumstances is expected and not a bug. While we've made optimizations and there may be more in the future, the way :has is designed to operate will never perform in linear time in all circumstances. Users should thoughtfully apply the use of :has.

The anchor selector (the A in A:has(B)) should not be an element that has too many children. Additionally, too general an anchor, such as *, can cause :has() to be applied to every element.

Further, it is recommended that the inner selector (the B in A:has(B)) should use combinators like > or + to limit traversal. When the selector inside :has() is not tightly constrained, Soup Sieve might need to traverse the entire subtree of the anchor element to check the condition.

If the risk for a given project is too great, user input can be scanned for usage of :has and be denied. Calls can also be wrapped in a timeout to prevent long-running processes as well.

Related Vulnerabilities

Other vulnerabilities affecting the same packages

High Risk
15 hours ago
Soup Sieve: Polynomial-time denial of service in the descendant combinator
0.4 and 0.5 and 0.5.1 - 0.5.3 and 0.6 and 1.0b1 and 1.0b2 and 1.0 and 1.0.1 - 1.0.2 and 1.1 and 1.2 and 1.2.1 and 1.3 and 1.3.1 and 1.4 and 1.5 and 1.6 and 1.6.1 - 1.6.2 and 1.7 and 1.7.1 - 1.7.3 and 1.8 and 1.9 and 1.9.1 - 1.9.6 and 2.0 and 2.0.1 and 2.1 and 2.2 and 2.2.1 and 2.3 and 2.3.1 - 2.3.2 and 2.3.2.post1 and 2.4 and 2.4.1 and 2.5 and 2.6 and 2.7 and 2.8 and 2.8.1 - 2.8.4 and 2.9 and 2.9.1 - 2.9.2 and 2.10 and 3.0 and 3.0.1 - 3.0.4 GHSA-mmhr-3c5m-f64f
0.4 and 0.5 and 0.5.1 - 0.5.3 and 0.6 and 1.0b1 and 1.0b2 and 1.0 and 1.0.1 - 1.0.2 and 1.1 and 1.2 and 1.2.1 and 1.3 and 1.3.1 and 1.4 and 1.5 and 1.6 and 1.6.1 - 1.6.2 and 1.7 and 1.7.1 - 1.7.3 and 1.8 and 1.9 and 1.9.1 - 1.9.6 and 2.0 and 2.0.1 and 2.1 and 2.2 and 2.2.1 and 2.3 and 2.3.1 - 2.3.2 and 2.3.2.post1 and 2.4 and 2.4.1 and 2.5 and 2.6 and 2.7 and 2.8 and 2.8.1 - 2.8.4 and 2.9 and 2.9.1 - 2.9.2 and 2.10 and 3.0 and 3.0.1 - 3.0.4 GHSA-mmhr-3c5m-f64f
Medium Risk
1 month ago
Soup Sieve: Polynomial-time ReDoS (O(n²)) in the `IDENTIFIER` / `VALUE` selector sub-patterns
0.4 and 0.5 and 0.5.1 - 0.5.3 and 0.6 and 1.0b1 and 1.0b2 and 1.0 and 1.0.1 - 1.0.2 and 1.1 and 1.2 and 1.2.1 and 1.3 and 1.3.1 and 1.4 and 1.5 and 1.6 and 1.6.1 - 1.6.2 and 1.7 and 1.7.1 - 1.7.3 and 1.8 and 1.9 and 1.9.1 - 1.9.6 and 2.0 and 2.0.1 and 2.1 and 2.2 and 2.2.1 and 2.3 and 2.3.1 - 2.3.2 and 2.3.2.post1 and 2.4 and 2.4.1 and 2.5 and 2.6 and 2.7 and 2.8 and 2.8.1 - 2.8.4 PYSEC-2026-4170
0.4 and 0.5 and 0.5.1 - 0.5.3 and 0.6 and 1.0b1 and 1.0b2 and 1.0 and 1.0.1 - 1.0.2 and 1.1 and 1.2 and 1.2.1 and 1.3 and 1.3.1 and 1.4 and 1.5 and 1.6 and 1.6.1 - 1.6.2 and 1.7 and 1.7.1 - 1.7.3 and 1.8 and 1.9 and 1.9.1 - 1.9.6 and 2.0 and 2.0.1 and 2.1 and 2.2 and 2.2.1 and 2.3 and 2.3.1 - 2.3.2 and 2.3.2.post1 and 2.4 and 2.4.1 and 2.5 and 2.6 and 2.7 and 2.8 and 2.8.1 - 2.8.4 PYSEC-2026-4170
Medium Risk
1 month ago
Soup Sieve: Polynomial-time ReDoS (O(n²)) in the whitespace/comment trimming regex `RE_WS_END` (triggers on VALID selectors)
0.4 and 0.5 and 0.5.1 - 0.5.3 and 0.6 and 1.0b1 and 1.0b2 and 1.0 and 1.0.1 - 1.0.2 and 1.1 and 1.2 and 1.2.1 and 1.3 and 1.3.1 and 1.4 and 1.5 and 1.6 and 1.6.1 - 1.6.2 and 1.7 and 1.7.1 - 1.7.3 and 1.8 and 1.9 and 1.9.1 - 1.9.6 and 2.0 and 2.0.1 and 2.1 and 2.2 and 2.2.1 and 2.3 and 2.3.1 - 2.3.2 and 2.3.2.post1 and 2.4 and 2.4.1 and 2.5 and 2.6 and 2.7 and 2.8 and 2.8.1 - 2.8.4 PYSEC-2026-4171
0.4 and 0.5 and 0.5.1 - 0.5.3 and 0.6 and 1.0b1 and 1.0b2 and 1.0 and 1.0.1 - 1.0.2 and 1.1 and 1.2 and 1.2.1 and 1.3 and 1.3.1 and 1.4 and 1.5 and 1.6 and 1.6.1 - 1.6.2 and 1.7 and 1.7.1 - 1.7.3 and 1.8 and 1.9 and 1.9.1 - 1.9.6 and 2.0 and 2.0.1 and 2.1 and 2.2 and 2.2.1 and 2.3 and 2.3.1 - 2.3.2 and 2.3.2.post1 and 2.4 and 2.4.1 and 2.5 and 2.6 and 2.7 and 2.8 and 2.8.1 - 2.8.4 PYSEC-2026-4171
Medium Risk
1 month ago
Soup Sieve: Polynomial-time ReDoS (O(n²)) in the `IDENTIFIER` / `VALUE` selector sub-patterns
0.4 and 0.5 and 0.5.1 - 0.5.3 and 0.6 and 1.0b1 and 1.0b2 and 1.0 and 1.0.1 - 1.0.2 and 1.1 and 1.2 and 1.2.1 and 1.3 and 1.3.1 and 1.4 and 1.5 and 1.6 and 1.6.1 - 1.6.2 and 1.7 and 1.7.1 - 1.7.3 and 1.8 and 1.9 and 1.9.1 - 1.9.6 and 2.0 and 2.0.1 and 2.1 and 2.2 and 2.2.1 and 2.3 and 2.3.1 - 2.3.2 and 2.3.2.post1 and 2.4 and 2.4.1 and 2.5 and 2.6 and 2.7 and 2.8 and 2.8.1 - 2.8.4 GHSA-gjv8-xp57-g29c
0.4 and 0.5 and 0.5.1 - 0.5.3 and 0.6 and 1.0b1 and 1.0b2 and 1.0 and 1.0.1 - 1.0.2 and 1.1 and 1.2 and 1.2.1 and 1.3 and 1.3.1 and 1.4 and 1.5 and 1.6 and 1.6.1 - 1.6.2 and 1.7 and 1.7.1 - 1.7.3 and 1.8 and 1.9 and 1.9.1 - 1.9.6 and 2.0 and 2.0.1 and 2.1 and 2.2 and 2.2.1 and 2.3 and 2.3.1 - 2.3.2 and 2.3.2.post1 and 2.4 and 2.4.1 and 2.5 and 2.6 and 2.7 and 2.8 and 2.8.1 - 2.8.4 GHSA-gjv8-xp57-g29c
Medium Risk
1 month ago
Soup Sieve: Polynomial-time ReDoS (O(n²)) in the whitespace/comment trimming regex `RE_WS_END` (triggers on VALID selectors)
0.4 and 0.5 and 0.5.1 - 0.5.3 and 0.6 and 1.0b1 and 1.0b2 and 1.0 and 1.0.1 - 1.0.2 and 1.1 and 1.2 and 1.2.1 and 1.3 and 1.3.1 and 1.4 and 1.5 and 1.6 and 1.6.1 - 1.6.2 and 1.7 and 1.7.1 - 1.7.3 and 1.8 and 1.9 and 1.9.1 - 1.9.6 and 2.0 and 2.0.1 and 2.1 and 2.2 and 2.2.1 and 2.3 and 2.3.1 - 2.3.2 and 2.3.2.post1 and 2.4 and 2.4.1 and 2.5 and 2.6 and 2.7 and 2.8 and 2.8.1 - 2.8.4 GHSA-j934-xhv5-fg8f
0.4 and 0.5 and 0.5.1 - 0.5.3 and 0.6 and 1.0b1 and 1.0b2 and 1.0 and 1.0.1 - 1.0.2 and 1.1 and 1.2 and 1.2.1 and 1.3 and 1.3.1 and 1.4 and 1.5 and 1.6 and 1.6.1 - 1.6.2 and 1.7 and 1.7.1 - 1.7.3 and 1.8 and 1.9 and 1.9.1 - 1.9.6 and 2.0 and 2.0.1 and 2.1 and 2.2 and 2.2.1 and 2.3 and 2.3.1 - 2.3.2 and 2.3.2.post1 and 2.4 and 2.4.1 and 2.5 and 2.6 and 2.7 and 2.8 and 2.8.1 - 2.8.4 GHSA-j934-xhv5-fg8f
View all vulnerabilities for these packages

Impacted packages

Timeline

Published
15 hours ago
November 13, 2026 at 12:45 AM UTC
Last Modified
15 hours ago
November 13, 2026 at 01:00 AM UTC