CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-126192

CVE-2026-126192: Quadratic Algorithmic Complexity Denial of Service in Soup Sieve CSS Selector Engine

Alon Barad
Alon Barad
Software Engineer

Nov 13, 2026·10 min read·2 visits

Executive Summary (TL;DR)

Soup Sieve versions prior to 2.10 exhibit O(n^2) quadratic time complexity when matching CSS selectors that combine the :has() pseudo-class with the general sibling combinator (~), such as div:has(a ~ b). Submitting a ~56 KB HTML payload containing 8,000 sibling elements without the terminating match locks a CPU core for approximately 60 seconds, enabling remote Denial of Service. Upgrading to soupsieve 2.10 resolves the issue via sibling index caching.

CVE-2026-126192 is an algorithmic complexity vulnerability in Soup Sieve (soupsieve), the default CSS selector engine for Beautiful Soup 4 in Python. Evaluating a CSS selector that contains a general sibling combinator (~) nested inside a :has() relational pseudo-class against attacker-controlled HTML markup triggers O(n^2) quadratic evaluation complexity when the right-hand sibling target is absent. An unauthenticated attacker can supply small HTML documents containing thousands of sibling elements to exhaust CPU resources and stall application worker threads.

Vulnerability Overview

Soup Sieve (soupsieve) is the underlying CSS selector library integrated into Python's Beautiful Soup 4 (bs4) HTML and XML parsing package. It compiles CSS selector strings into internal abstract syntax structures and executes tree-traversal routines (select, select_one, iselect, match, filter, and closest) across parsed document object models. Web scrapers, sanitization pipelines, document converters, and content ingestion services rely on Beautiful Soup and Soup Sieve to extract and filter HTML elements.

In versions prior to 2.10, Soup Sieve contains an inefficient algorithmic complexity flaw classified under CWE-407 (Inefficient Algorithmic Complexity) and CWE-400 (Uncontrolled Resource Consumption). The vulnerability manifests when evaluating a CSS selector that uses a general sibling combinator (~) inside a :has() relational pseudo-class, such as div:has(a ~ b).

When the engine processes this selector pattern against an HTML parent node containing $n$ matching left-hand child elements (<a>) but no matching right-hand sibling element (<b>), it fails to memoize prior sibling traversal results. Consequently, the engine performs repeated linear scans across the remaining sibling list for every candidate element.

This behavior degrades selector evaluation from linear $\mathcal{O}(n)$ time to quadratic $\mathcal{O}(n^2)$ time relative to the number of sibling tags. If an application applies affected selectors to untrusted HTML provided by external users or remote servers, an attacker can trigger prolonged CPU saturation and Denial of Service (DoS) using minimal network bandwidth.

Root Cause Analysis

In the CSS Selectors Level 4 specification, the :has() relational pseudo-class matches an anchor element if any relative selector argument matches at least one element relative to that anchor. When evaluating a selector such as div:has(a ~ b), Soup Sieve first identifies a candidate <div> element and invokes CSSMatch.match_future_child() in soupsieve/css_match.py to traverse its descendants looking for the left-hand sub-selector (a).

Each time match_future_child() locates a matching <a> element, Soup Sieve evaluates the relative relationship to determine whether a subsequent sibling <b> exists under the same parent. Internally, the general sibling combinator inside :has() is represented by the constant REL_HAS_SIBLING, which routes execution to CSSMatch.match_future_relations().

Prior to version 2.10, the REL_HAS_SIBLING handler inside match_future_relations() executed an uncached while loop that stepped forward through every subsequent sibling tag via self.get_next_tag(sibling) until a tag matched b or the sibling chain ended. Simultaneously, the standard past general sibling combinator (REL_SIBLING in match_past_relations()) executed a similar uncached backward walk via self.get_previous_tag(sibling).

When the target element b is absent from the DOM subtree, the inner loop cannot short-circuit early. For a parent element containing $n$ sibling <a> elements and zero <b> elements, the first <a> scans $(n - 1)$ siblings, the second scans $(n - 2)$ siblings, and the $(n - 1)$-th scans $1$ sibling. The total number of tag comparisons equals:

$$\sum_{k=1}^{n-1} (n - k) = \frac{n(n - 1)}{2} = \mathcal{O}(n^2)$$

In contrast, the adjacent sibling combinator (+, represented by REL_HAS_CLOSE_SIBLING in div:has(a + b)) inspects only the single immediate next tag via one call to self.get_next_tag(el). Because it does not iterate across the remainder of the sibling list, the + combinator executes in strictly linear $\mathcal{O}(n)$ time.

Code Analysis

The vulnerability resides in soupsieve/css_match.py within the CSSMatch class. In versions <= 2.9.2, both match_past_relations() and match_future_relations() traversed sibling chains statelessly on every invocation:

# Vulnerable implementation in soupsieve/css_match.py (< 2.10)
def match_future_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> bool:
    found = False
    if relation[0] is ct.Null:
        return found
 
    if relation[0].rel_type == REL_HAS_PARENT:
        found = self.match_future_child(el, relation, True)
    elif relation[0].rel_type == REL_HAS_CLOSE_PARENT:
        found = self.match_future_child(el, relation)
    elif relation[0].rel_type == REL_HAS_SIBLING:
        # VULNERABLE: Walks every subsequent sibling tag from 'el' to the end of the parent.
        # No state is saved across calls for siblings sharing the same parent.
        sibling = self.get_next_tag(el)
        while not found and sibling:
            found = self.match_selectors(sibling, relation)
            sibling = self.get_next_tag(sibling)
    elif relation[0].rel_type == REL_HAS_CLOSE_SIBLING:
        sibling = self.get_next_tag(el)
        if sibling and self.is_tag(sibling):
            found = self.match_selectors(sibling, relation)
    return found

Commit ffb88cca745c591224f74a8e86414a85e8f35fc0 resolves the issue by introducing a per-parent sibling cache (self.sib_cache) and delegating both REL_SIBLING and REL_HAS_SIBLING evaluations to a new helper method, match_general_sibling():

# Patched implementation in soupsieve/css_match.py (2.10)
def match_general_sibling(self, el: bs4.Tag, relation: ct.SelectorList) -> bool:
    """Match general sibling combinator."""
    found = False
    if relation[0] is ct.Null:
        return found
 
    pkey: tuple[str | None, int] | None = None
    key: tuple[ct.SelectorList, int] | None = None
 
    parent = self.get_parent(el)
    if parent is None:
        return found
 
    if parent:
        # Key the cache by parent tag name and object identity
        pkey = (parent.name, id(parent))
        if pkey not in self.sib_cache:
            self.sib_cache[pkey] = {}
 
    # For REL_HAS_SIBLING (reverse=True), scan backward from the last child.
    # For REL_SIBLING (reverse=False), scan forward from the first child.
    reverse = relation[0].rel_type == REL_HAS_SIBLING
    start = len(parent) - 1 if reverse else 0
    if pkey:
        key = (relation, id(relation))
        if key in self.sib_cache[pkey]:
            index = self.sib_cache[pkey][key]
            if index >= 0:
                # A matching sibling exists at 'index'; check if 'el' precedes/follows it
                a, b = (index, start) if reverse else (start, index)
                return not within(el, parent, a, b)
            else:
                # Cached -1 indicates no matching sibling exists under this parent
                return False
        self.sib_cache[pkey][key] = start
 
    passed = False
    incr = -1 if reverse else 1
    for child in self.get_children(parent, start=start, reverse=reverse):
        start += incr
        if not isinstance(child, bs4.Tag):
            continue
        if child is el:
            passed = True
        if self.match_selectors(child, relation):
            found = True
            break
 
    # Cache the index of the furthest matching sibling, or -1 if none exists
    if pkey and key:
        self.sib_cache[pkey][key] = start if found else -1
 
    if passed:
        found = False
 
    return found

The patched algorithm changes both the traversal direction and the state management. For REL_HAS_SIBLING, match_general_sibling() starts at the end of the parent's child list (len(parent) - 1) and walks backward toward the first child to locate the furthest matching sibling (b). If a match is found at child index $k$, that index is stored in self.sib_cache[pkey][key], allowing all subsequent sibling checks under the same parent to simply compare their position against index $k$ via within(). If no match exists under the parent, -1 is stored in self.sib_cache[pkey][key], and all remaining n - 1 sibling candidates return False in $\mathcal{O}(1)$ constant time.

> [!NOTE] > In addition to the caching fix, version 2.10 includes commit b416f7d69dc12d8e7ac04c0da99b488827e24220, which adds an ignore parameter to Soup Sieve's public API functions (compile, select, match, etc.). This allows applications that accept user-supplied CSS selectors to explicitly block complex relational pseudo-classes such as :has.

Exploitation Methodology

Exploiting CVE-2026-126192 requires two conditions: the target application must evaluate a CSS selector containing a general sibling combinator nested within :has() (or evaluate user-supplied selectors directly), and the application must parse untrusted HTML input through Beautiful Soup and Soup Sieve. No authentication or special privileges are required if the HTML ingestion endpoint is publicly reachable.

An attacker constructs an HTML document containing a parent container matched by the outer selector (such as <div>) populated with a dense sequence of sibling tags matching the left-hand selector (<a></a>), while omitting the right-hand selector (<b>). Because HTML opening and closing tags for single-letter elements require only 7 bytes (<a></a>), an attacker can pack thousands of sibling nodes into a compact HTTP request body or webhook payload.

import time
import soupsieve
from bs4 import BeautifulSoup
 
# Selector containing a general sibling combinator inside :has()
selector = "div:has(a ~ b)"
 
for n in (1000, 2000, 4000, 8000):
    # Generate markup with 'n' matching left-hand siblings and no matching 'b' sibling
    html = "<div>" + ("<a></a>" * n) + "</div>"
    soup = BeautifulSoup(html, "html.parser")
    
    start = time.perf_counter()
    soupsieve.select(selector, soup)
    elapsed_ms = round((time.perf_counter() - start) * 1000)
    print(f"n={n:<5} -> {elapsed_ms:>6} ms")

Executing this proof-of-concept on soupsieve==2.9.2 demonstrates exact quadratic scaling where doubling the sibling count $n$ quadruples the execution time ($2^2 = 4$):

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

At $n = 8000$, the entire HTML payload is approximately 56 KB in size, which easily bypasses standard web server request body limits (typically 1 MB to 10 MB). When processed by a synchronous Python web worker (such as Gunicorn sync workers, uWSGI processes, or Celery task runners), the thread blocks on the GIL-bound CPU loop for nearly 60 seconds per request.

Impact Assessment

The primary impact of CVE-2026-126192 is application-level Denial of Service (MITRE ATT&CK technique T1499: Endpoint Denial of Service). The vulnerability does not permit memory corruption, arbitrary code execution, or unauthorized data disclosure (VC:N/VI:N), as all operations occur within memory-safe Python data structures.

In synchronous Python web frameworks (such as Django or Flask running under Gunicorn with standard worker counts of $2 \times \text{CPU cores} + 1$), an attacker sending a handful of concurrent 56 KB requests can occupy all available worker processes for a full minute. Legitimate incoming HTTP requests will queue at the reverse proxy layer and eventually fail with 502 Bad Gateway or 504 Gateway Timeout errors.

In asynchronous Python frameworks (such as FastAPI or aiohttp), calling soupsieve.select() directly inside an async def coroutine blocks the main asyncio event loop thread. A single 56 KB payload freezes the entire event loop for 60 seconds, preventing the server from handling any concurrent connections, health checks, or WebSocket heartbeats on that worker process.

The vulnerability is scored at 6.3 (Medium) under CVSS v4.0 (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N). The Attack Requirements metric is set to Present (AT:P) because exploitation depends on the target application using the specific :has(... ~ ...) selector pattern or allowing arbitrary user-controlled CSS selectors.

Remediation and Mitigation

The definitive remediation is to upgrade the soupsieve package to version 2.10 (2.10.0) or later. In version 2.10, the match_general_sibling() cache reduces the evaluation complexity of div:has(a ~ b) back to linear $\mathcal{O}(n)$ time, completing the $n = 8000$ proof-of-concept in milliseconds.

pip install --upgrade "soupsieve>=2.10.0"

If an immediate dependency upgrade is not feasible, developers can refactor affected CSS selectors to avoid nesting the general sibling combinator (~) inside :has(). Where the document structure permits, replacing the general sibling combinator with the adjacent sibling combinator (div:has(a + b)) avoids the quadratic loop even on unpatched versions of Soup Sieve. Additionally, narrowing broad anchor selectors (replacing *:has(a ~ b) with specific tag or class qualifiers) reduces the number of subtrees evaluated.

For applications that accept dynamic or user-authored CSS selectors and upgrade to soupsieve >= 2.10.0, developers should restrict expensive relational pseudo-classes using the newly introduced ignore parameter:

import soupsieve as sv
from bs4 import BeautifulSoup
 
soup = BeautifulSoup(untrusted_html, "html.parser")
 
# Reject selectors containing :has() or :-soup-contains()
compiled_selector = sv.compile(
    user_supplied_selector,
    ignore=[":has", ":-soup-contains"]
)
results = compiled_selector.select(soup)

Finally, services that parse untrusted HTML documents should enforce strict input size quotas on incoming markup and execute DOM parsing and selector matching within isolated worker processes configured with hard CPU execution timeouts (such as Celery time_limit settings or OS-level RLIMIT_CPU constraints).

Official Patches

Soup Sieve (GitHub)GitHub Security Advisory GHSA-4j63-6c4x-wfg4
Soup Sieve (GitHub)Pull Request #304 implementing general sibling caching
Soup Sieve (GitHub)Soup Sieve Release 2.10

Fix Analysis (1)

Technical Appendix

CVSS Score
6.3/ 10
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N

Affected Systems

Python applications using soupsieve < 2.10.0Python applications using Beautiful Soup 4 (bs4) with soupsieve < 2.10.0

Affected Versions Detail

Product
Affected Versions
Fixed Version
soupsieve
facelessuser
< 2.10.0 (0.4 through 2.9.2)2.10.0
AttributeDetail
CVE IDCVE-2026-126192 (GHSA-4j63-6c4x-wfg4)
Affected Packagesoupsieve (PyPI) < 2.10.0
Patched Versionsoupsieve 2.10.0
Vulnerability ClassCWE-407: Inefficient Algorithmic Complexity / CWE-400
CVSS v4.0 Score6.3 Medium (AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N)
Attack VectorNetwork (Untrusted HTML markup evaluated against :has(a ~ b) CSS selectors)
Primary ImpactApplication Denial of Service (O(n^2) CPU exhaustion)
Exploit StatusProof of Concept (PoC) publicly available; Not in CISA KEV

MITRE ATT&CK Mapping

T1499Endpoint Denial of Service
Impact
CWE-407
Inefficient Algorithmic Complexity

An algorithm in a product has an inefficient worst-case computational complexity that is detrimental to system performance and can be triggered by an attacker to cause resource exhaustion.

Vulnerability Timeline

Initial hardening commits introducing the ignore parameter for pseudo-class restrictions added to the development branch.
2026-09-20
Fix commit ffb88cca745c591224f74a8e86414a85e8f35fc0 and Pull Request #304 merged to cache general sibling combinator lookups.
2026-09-23
CVE-2026-126192 published by GitHub CNA and indexed in the National Vulnerability Database (NVD).
2026-11-12
GitHub Security Advisory GHSA-4j63-6c4x-wfg4 published and indexed in the OSV database.
2026-11-13

References & Sources

  • [1]GitHub Security Advisory GHSA-4j63-6c4x-wfg4
  • [2]Fix Commit ffb88cca745c591224f74a8e86414a85e8f35fc0
  • [3]Soup Sieve Pull Request #304
  • [4]Soup Sieve 2.10 Release Notes
  • [5]Official CVE.org Record for CVE-2026-126192
  • [6]National Vulnerability Database (NVD) Record for CVE-2026-126192
  • [7]Soup Sieve Upstream Repository
  • [8]MITRE ATT&CK Search Reference
  • [9]CISA Known Exploited Vulnerabilities Catalog

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

More Reports

•about 2 hours ago•CVE-2026-126190
7.5

CVE-2026-126190: Polynomial-Time Denial of Service in Soup Sieve CSS Descendant Combinator

CVE-2026-126190 (GHSA-mmhr-3c5m-f64f) is an inefficient algorithmic complexity vulnerability (CWE-407) in Soup Sieve, the default CSS selector library integrated into Beautiful Soup 4. In versions prior to 4.0, evaluating CSS selectors that contain descendant combinators (whitespace between compound selectors) executes an unmemoized upward traversal of the document tree. When evaluated against deeply nested HTML or XML documents, the time complexity scales polynomially at O(n^(k+1)), where n is the tree depth and k is the number of descendant combinators. Remote attackers can supply compact, deeply nested markup (1 KB to 4 KB) to trigger prolonged CPU starvation and denial of service in Python applications.

Amit Schendel
Amit Schendel
3 views•9 min read
•about 3 hours ago•CVE-2026-122006
9.8

CVE-2026-122006: Unauthenticated Arbitrary File Read and Write in Ruby on Rails Active Storage via ImageMagick

CVE-2026-122006 is a critical vulnerability in the Ruby on Rails Active Storage framework affecting versions prior to 8.0.5.2 and 8.1.4.1. When an application processes uploaded file attachments using the ImageMagick variant processor (:mini_magick), the framework invokes ImageMagick's metadata analyzer without applying a restrictive security policy. An unauthenticated attacker can upload a crafted file containing directives for unrestricted ImageMagick coders, such as Magick Scripting Language (MSL), resulting in arbitrary server-side file read and write operations. In deployments using the Disk storage service, attackers can exfiltrate sensitive application configuration files, including secret_key_base and master encryption keys, which enables remote code execution via signed payload forgery.

Alon Barad
Alon Barad
3 views•10 min read
•about 4 hours ago•CVE-2026-128065
6.9

CVE-2026-128065: Authorization Bypass via Unapplied RBAC Overrides in @grpc/grpc-js-xds

CVE-2026-128065 (also tracked as GHSA-gg5j-6742-v827) is an authorization bypass vulnerability affecting the @grpc/grpc-js-xds library in the Node.js gRPC ecosystem. In versions 1.14.0 and 1.14.1, XdsServer acknowledges Route and VirtualHost resources from an xDS control plane while failing to parse, store, or apply RBACPerRoute override configurations. When experimental RBAC is enabled and a broad top-level ALLOW policy is paired with restrictive VirtualHost or Route overrides, the server evaluates requests strictly against the top-level policy, permitting unauthorized RPC execution.

Alon Barad
Alon Barad
4 views•10 min read
•about 5 hours ago•CVE-2026-126188
3.7

CVE-2026-126188: Local Media Information Disclosure in Ruby on Rails Active Storage via FFmpeg Demuxer Selection

CVE-2026-126188 (GHSA-3f4q-8x5m-76v8) is an information disclosure vulnerability in the Active Storage component of Ruby on Rails prior to versions 8.0.5.2 and 8.1.4.1. When generating previews or extracting metadata from uploaded media files, Active Storage invokes the external command-line tools ffmpeg and ffprobe without specifying a format whitelist. An attacker who can upload a crafted HTTP Live Streaming (HLS) playlist while declaring a standard video or audio MIME type can cause FFmpeg to engage the HLS demuxer, resolve file:// segment URIs, and disclose visual frames or metadata from local media files readable by the Rails application process.

Amit Schendel
Amit Schendel
3 views•10 min read
•about 6 hours ago•CVE-2026-126206
5.3

CVE-2026-126206: Cross-Site Scripting via HTML5 Named Solidus Entity in Loofah URI Validator

CVE-2026-126206 (GHSA-jcff-cj73-4w4f) is a Cross-Site Scripting (XSS) vulnerability in version 2.25.2 of the Loofah Ruby gem. The vulnerability stems from an incomplete character reference normalization routine in the `Loofah::HTML5::Scrub.allowed_uri?` method, which fails to decode the HTML5 named character reference `&sol;` (representing the solidus character `/`). Consequently, a crafted `data:` URI such as `data:text&sol;html,<script>alert(1)</script>` bypasses mediatype validation by triggering the WHATWG MIME parser fallback to `text/plain`, while web browsers later resolve `&sol;` to `/` and execute the payload as `text/html`.

Alon Barad
Alon Barad
3 views•9 min read
•about 7 hours ago•CVE-2026-125325
5.9

CVE-2026-125325: Cross-Request Information Disclosure via Shared BM25 Index Race Condition in pgEdge RAG Server

pgEdge RAG Server versions prior to 2.0.0 contain a race condition in the hybrid search pipeline orchestrator (CWE-362, CWE-488). The server maintains a single shared, in-memory BM25 index instance across all incoming HTTP requests on a pipeline. Concurrent queries to the unauthenticated HTTP REST API interleave non-atomic clear, document addition, and search operations on this singleton index. As a result, documents fetched using one caller's search filter can be queried and returned to another concurrent caller, bypassing multi-tenant data isolation without generating any anomalous audit trail in server or database logs.

Amit Schendel
Amit Schendel
2 views•10 min read