Sep 18, 2026·7 min read·5 visits
Unauthenticated remote attackers can cause denial of service (CPU exhaustion) via crafted CSS selectors in Soup Sieve's parser before version 2.9.
A regular expression denial of service (ReDoS) vulnerability in Soup Sieve prior to version 2.9 allows remote attackers to cause CPU exhaustion and service disruption. The issue lies within the definition of the IDENTIFIER and VALUE selector sub-patterns in the CSS parser component, which uses overlapping adjacent quantified groups. When parsing long, crafted, or unclosed CSS selectors, backtracking-based regular expression engines experience quadratic performance degradation. User-controlled selectors can reach this path through soupsieve.compile(), soupsieve.select(), or BeautifulSoup.select(), while applications using only hard-coded selectors are unaffected.
The Soup Sieve library is a highly utilized component in the Python ecosystem, serving as the default CSS selector implementation for the ubiquitous Beautiful Soup 4 library. It allows developers to search and filter parsed HTML/XML documents using complex CSS selector patterns, mirroring modern browser capability. Because of this, Soup Sieve is commonly integrated into automated data ingestion systems, web scrapers, data aggregators, and document processing backends. These applications frequently accept and process external, user-supplied data, making selector parsing a critical boundary for system stability and security.
Prior to version 2.9, Soup Sieve's selector compiler contained a vulnerability within its regular expression patterns. An unauthenticated attacker capable of providing a custom, crafted CSS selector string could trigger a massive resource consumption state. This is tracked as CVE-2026-86000 and is classified under CWE-1333 (Inefficient Regular Expression Complexity) and CWE-400 (Uncontrolled Resource Consumption).
The vulnerability is activated when Soup Sieve processes custom selectors using functions such as soupsieve.compile(), soupsieve.select(), or BeautifulSoup.select(). When a long or unclosed CSS selector pattern is compiled, the backtracking regular expression engine attempts to resolve overlapping paths. This results in quadratic time complexity ($O(n^2)$) relative to the length of the string, keeping the system process bound to the CPU, locking the Global Interpreter Lock, and completely stalling service for any other tasks.
To understand the technical root cause of CVE-2026-86000, we must examine how the Soup Sieve parser processes CSS identifiers. In src/soupsieve/css_parser.py, the tokenization engine depends on a hand-written regular expression sub-pattern named IDENTIFIER. This pattern is responsible for matching selectors such as tags, IDs, class names, and attributes.
The vulnerable definition of IDENTIFIER was structured with two adjacent quantified sub-groups. The first group, representing the identifier prefix, was defined as (?:-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})+|--). The second group, matching the identifier continuation, was defined as (?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})*. The critical vulnerability resides in the fact that these two adjacent groups match heavily overlapping character classes.
The character classes in both groups permit standard alphanumeric characters, such as lowercase and uppercase letters (a-z, A-Z) and underscores (_). When a long string consisting entirely of overlapping characters is evaluated, the Python standard re module (which implements a backtracking NFA-based engine) experiences high ambiguity. It cannot deterministically determine where the first group (quantified with +) ends and where the second group (quantified with *) begins.
If the input eventually fails downstream—such as when a long alphanumeric identifier is terminated with an invalid character like !—the NFA engine is forced to backtrack. It evaluates every possible split of characters between the first and second groups to find a valid match. The number of splits grows quadratically ($O(n^2)$) relative to the length of the string. This leads to massive CPU exhaustion, pinning a CPU core at 100% until the evaluation is complete.
Comparing the code of src/soupsieve/css_parser.py before and after the version 2.9 release demonstrates a clean remediation of this regular expression complexity. The vulnerability was resolved in commit ce44e4996e6632871c18cdd7a7fb641be8ef34ef by making the prefix matching deterministic.
Before the patch, the IDENTIFIER regular expression was defined as follows:
# Vulnerable code in soupsieve/css_parser.py
IDENTIFIER = fr'''
(?:(?:-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})+|--)
(?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})*)
'''The presence of the + quantifier in the first group allows it to consume an arbitrary length of valid identifier characters. Because the second group also consumes standard identifier characters via the * quantifier, the engine is forced to explore a massive state space when backtracking occurs.
The patch successfully resolved the flaw by refactoring the expression to remove the variable quantifier from the prefix group:
# Patched code in soupsieve/css_parser.py (v2.9)
IDENTIFIER = fr'''
(?:(?:--|-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES}))
(?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})*)
'''By replacing + with a deterministic requirement matching exactly one character or double hyphens (--), the engine is forced to consume precisely one character in the prefix group. Any remaining characters are consumed strictly by the continuation group. This completely eliminates the ambiguity, changing the matching operation's time complexity from $O(n^2)$ back to linear $O(n)$ time. This fix is complete and robust because it removes the foundational mechanism required for catastrophic backtracking.
Exploiting CVE-2026-86000 is straightforward and requires no special system privileges. An attacker simply needs to submit a crafted CSS selector to any system that processes inputs with the vulnerable Soup Sieve library. Since the vulnerability is located inside the tokenizing engine, simply compiling a malformed selector is sufficient to trigger the CPU hang.
There are two main payload variants that trigger the backtracking behaviour. The first is a plain identifier payload where a very long string of alphanumeric characters is terminated with a syntax-breaking character (like an exclamation mark). This forces the engine to parse the long string as an identifier, reach the end, realize the match has failed, and backtrack through the entire string:
/* Plain identifier payload */
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa... [10,000+ chars] ...aaa!The second vector involves unquoted attribute-value selectors. The Soup Sieve parser embeds the vulnerable IDENTIFIER definition inside the pattern used to evaluate attribute values. If an attacker submits an unclosed attribute selector containing a long run of unquoted characters, the engine experiences identical backtracking during EOF resolution:
/* Unquoted attribute-value payload */
[a=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa... [12,000+ chars]When a Python process executes either payload, the regular expression engine holds the Python Global Interpreter Lock (GIL). This prevents any other threads in the same process from executing, stalling the entire application worker. An attacker can easily weaponize this by sending multiple concurrent requests to fully saturate all worker processes on the application server, resulting in complete denial of service.
The primary impact of CVE-2026-86000 is a complete Denial of Service (DoS) of the affected Python application. Because the execution of the regular expression engine occurs within native C code in the Python standard library, it runs at maximum processor speed, pinning a CPU core at 100%. If the host application is single-process or has limited worker threads, a single malicious request can completely lock up the service.
When the regular expression engine is forced to backtrack over a malicious payload, it consumes 100% of the allocated CPU core. Because Python's Global Interpreter Lock (GIL) is held during the C-level execution of the re module, even multi-threaded application servers (such as those running under certain configurations of gunicorn or uWSGI) can experience complete lockups. The entire process becomes unresponsive to health checks and incoming user traffic.
In environments where multiple worker processes are deployed, an attacker can trivially send a small number of concurrent malicious requests (matching the number of worker processes) to completely exhaust the application pool. This results in an immediate, sustained denial of service across the entire platform. Since the exploit complexity is low and requires no special privileges, systems that parse arbitrary, user-supplied HTML documents or support custom selectors are highly exposed.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L| Product | Affected Versions | Fixed Version |
|---|---|---|
soupsieve facelessuser | < 2.9 | 2.9 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-1333 |
| Attack Vector | Network (AV:N) |
| CVSS v3.1 Score | 5.3 (Medium) |
| EPSS Score | Not Available |
| Primary Impact | Denial of Service (CPU Exhaustion) |
| Exploit Status | Proof-of-Concept Available |
| CISA KEV Status | Not Listed |
The product uses a regular expression that can take a very long time to evaluate when faced with specific inputs, leading to a Denial of Service.
A polynomial-time Regular Expression Denial of Service (ReDoS) vulnerability in Soup Sieve versions prior to 2.9 allows remote unauthenticated attackers to cause CPU exhaustion and thread-pool denial of service. The vulnerability resides in the trailing whitespace and comment preprocessing step of the CSS parser. An attacker can trigger quadratic backtracking by submitting a crafted CSS selector string containing a long run of internal spaces or comments terminated by a non-matching token. This blocks the Python Global Interpreter Lock (GIL) and halts worker threads.
A protocol-level validation bypass in CoreDNS versions prior to 1.14.7 allows unauthenticated remote attackers to proxy unauthorized DNS UPDATE messages (Opcode 5) using modern alternative transport layers such as DoH, DoH3, DoQ, and gRPC. If upstream authoritative servers trust the CoreDNS server's source IP and do not enforce TSIG authentication, attackers can inject, alter, or delete DNS zone records, leading to potential zone takeover or traffic redirection.
A path traversal vulnerability exists in Grav CMS versions prior to 2.0.16. The flaw occurs within the file validation mechanisms of the MediaUploadTrait, enabling authenticated users with media management privileges to bypass sandbox limitations. This allows the deletion of arbitrary files on the filesystem, which can result in denial of service or remote code execution.
An unauthenticated directory traversal vulnerability exists in Grav CMS prior to version 2.0.15. Due to an insecure string-based containment check (str_starts_with) in the pre-boot static asset server, attackers can read files in sibling directories sharing a prefix with the configured asset path when plugin-asset-map.php is enabled.
CVE-2026-75828 is a critical stored cross-site scripting (XSS) vulnerability in the getgrav Grav CMS before version 2.0.15. The vulnerability resides in the detectXss() security filter mechanism, where parser-differential mismatches between the regular-expression-based server-side validation and browser HTML5 tokenization allow authenticated editors to bypass event-handler detection and inject arbitrary JavaScript execution vectors.
An arbitrary file write and remote code execution vulnerability exists in Grav CMS before version 2.0.15. The vulnerability is caused by using an incomplete denylist validation approach for bare PHP functions in the Blueprint dynamic-data compiler, allowing authenticated users with page-editing or blueprint-configuration privileges to execute arbitrary functions such as error_log.