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

Published Nov 12, 2026
·
Updated

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#performanceconsiderations.

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: matchfuturechild walks every candidate a under the scope, and for each one the general-sibling branch of matchfuturerelations 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.

python 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.perfcounter() soupsieve.select(selector, soup) print(n, round((time.perfcounter() - 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.

Other sources

Soup Sieve is a CSS selector library designed to be used with Beautiful Soup 4. Prior to 2.10, matching a developer-authored selector that places the general sibling combinator inside :has() repeatedly scans each candidate element's following siblings when the right-hand side is absent, producing quadratic work on attacker-controlled HTML. Applications that evaluate a fixed selector such as div:has(a ~ b) over untrusted markup can have a worker occupied for minutes by a small document, while the adjacent sibling form and trusted-only document processing are not affected in the same way. This issue is fixed in version 2.10.

— MITRE

Affected Software

2 affected componentsFixes available
pip/soupsieve<=2.9.2
2.10.0
pip/soupsieve<2.10.0
2.10.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/soupsieve to a version that resolves this vulnerability.

    Fixed in 2.10.0
  2. Upgrade

    Upgrade soupsieve to a version that resolves this vulnerability.

    Fixed in 2.10
  3. Configuration

    Constrain :has() selectors to limit traversal, preferably using child (>) or adjacent-sibling (+) combinators, and avoid overly general anchors such as *.

    Soup Sieve selectors :has() selector structure = Use tightly constrained inner selectors with > or + instead of a general-sibling ~ combinator; avoid overly general anchors such as * and anchors with too many children
  4. Compensating control

    Scan user input for use of :has and deny input containing it when the risk is unacceptable.

  5. Operational

    Wrap selector evaluation in a timeout to prevent long-running processes.

Event History

Nov 12, 2026
CVE Published
via MITRE·05:09 PM
Data Sourced
via OSV·05:09 PM
DescriptionSeverityWeakness
Data Sourced
via MITRE·05:09 PM
DescriptionWeakness
Data Sourced
via NVD·05:17 PM
DescriptionSeverityWeakness
Nov 13, 2026
Exploit Published
via CISA·12:20 AM
Advisory Published
via GitHub·12:45 AM
Data Sourced
via GitHub·12:45 AM
DescriptionWeaknessAffected Software
Data Sourced
via OSV·12:45 AM
Affected Software

Frequently Asked Questions

1

Which applications are realistically exposed to this issue?

Applications using Soup Sieve before 2.10 are exposed when they evaluate a developer-authored selector containing a general sibling combinator inside :has() against attacker-controlled HTML. A fixed selector such as div:has(a ~ b) is an example.

2

What conditions are needed for exploitation?

An attacker needs to supply HTML that is processed by an affected selector and arranged so the selector's right-hand side is absent. This causes repeated scans of following siblings for each candidate element, creating quadratic work.

3

Are all :has() selectors and sibling combinators affected?

No. The issue specifically concerns the general sibling combinator inside :has() when its right-hand side is absent. The adjacent sibling form is not affected in the same way.

4

What should be done if immediate upgrading is not possible?

Avoid evaluating selectors that place the general sibling combinator inside :has() on untrusted HTML. Processing only trusted documents is also not affected in the same way.

5

How can an affected deployment be remediated?

Upgrade Soup Sieve to version 2.10, which fixes the issue.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203