Back to Blog
high SEVERITY5 min read

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.

O
By Orbis AppSec
Published July 11, 2026Reviewed July 11, 2026

Answer Summary

CVE-2026-49476 is a memory exhaustion vulnerability in Python's soupsieve library (versions ≤2.8.3) where large comma-separated CSS selector lists cause unbounded memory allocation (CWE-400: Uncontrolled Resource Consumption). The fix is upgrading soupsieve to version 2.8.4, which implements proper limits on selector list parsing to prevent memory exhaustion.

Vulnerability at a Glance

cweCWE-400 (Uncontrolled Resource Consumption)
fixUpgrade soupsieve from 2.8.3 to 2.8.4 in backend/uv.lock
riskDenial of service through memory exhaustion when processing crafted CSS selectors
languagePython
root causesoupsieve 2.8.3 had no bounds on memory allocation when parsing comma-separated CSS selector lists
vulnerabilityMemory Exhaustion via Large Comma-Separated Selector Lists

How Memory Exhaustion via Large Comma-Separated Selector Lists Happens in Python Soupsieve and How to Fix It

Introduction

In a production backend application, we discovered a high-severity memory exhaustion vulnerability lurking in the backend/uv.lock dependency file. The culprit: soupsieve version 2.8.3, a CSS selector parsing library that underpins BeautifulSoup's select() functionality. CVE-2026-49476 reveals that soupsieve's parser had no effective bounds on memory allocation when processing large comma-separated CSS selector lists—meaning any code path where user-influenced data reaches a CSS selector query could be weaponized to crash the application.

This matters for any Python developer using BeautifulSoup for HTML/XML parsing, especially in web scrapers, content management systems, or any service that accepts user-defined CSS selectors.

The Vulnerability Explained

Soupsieve is the CSS selector engine behind BeautifulSoup4's .select() and .select_one() methods. When you write:

from bs4 import BeautifulSoup

soup = BeautifulSoup(html_content, 'html.parser')
results = soup.select("div.class1, div.class2, div.class3")

Soupsieve parses that comma-separated selector list internally. In version 2.8.3, the parsing algorithm allocated memory proportional to the number and complexity of comma-separated selectors without any upper bound.

The Attack Scenario

Consider a backend CLI tool or web service that accepts CSS selectors as input—perhaps for content extraction, scraping configuration, or template processing. An attacker could supply a selector like:

a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, ... [repeated thousands of times]

Or more insidiously, nested combinators repeated across thousands of comma-separated groups:

div > span + a ~ p, div > span + a ~ p, div > span + a ~ p, ... [×100,000]

Each selector in the comma-separated list triggers internal data structure allocation. With soupsieve 2.8.3, there was no limit on how many selectors could be processed, and the internal representation grew linearly (or worse) with the input. This could exhaust available memory, causing:

  1. Application crash via MemoryError
  2. System-wide degradation as the OS begins swapping
  3. Cascading failures in containerized environments where memory limits trigger OOM kills

Threat Model for This Application

The PR notes this is a local CLI tool where "exploitation requires the attacker to control command-line arguments or input files." This means an attacker who can influence input files (e.g., configuration files, scraped HTML with embedded selectors, or piped input) could trigger the vulnerability. While the attack surface is narrower than a public-facing web service, it's still exploitable in CI/CD pipelines, shared development environments, or when processing untrusted data files.

The Fix

The fix is a targeted dependency upgrade in backend/uv.lock:

Before (vulnerable):

[[package]]
name = "soupsieve"
version = "2.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627 }
wheels = [
    { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016 },
]

After (fixed):

[[package]]
name = "soupsieve"
version = "2.8.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110 }
wheels = [
    { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304 },
]

What Changed Upstream

Soupsieve 2.8.4 (released 2026-05-24) introduces internal limits on the number of selectors that can appear in a comma-separated list and optimizes memory allocation during parsing. The package size grew from 37,016 bytes to 37,304 bytes (a 288-byte increase), reflecting the added bounds-checking logic.

The fix is minimal and non-breaking—existing valid CSS selectors continue to work identically. Only pathologically large selector lists (those designed to exhaust memory) are now rejected or handled with bounded resources.

Prevention & Best Practices

  1. Keep dependencies updated: Use automated dependency scanning (Trivy, Dependabot, Snyk) to catch known CVEs in your lock files. This vulnerability was in a transitive dependency—you might not even realize you're using soupsieve directly.

  2. Validate user-controlled selectors: If your application accepts CSS selectors from users, impose reasonable length limits before passing them to BeautifulSoup:
    ```python
    MAX_SELECTOR_LENGTH = 1024
    MAX_SELECTOR_COUNT = 50

def safe_select(soup, selector_string):
if len(selector_string) > MAX_SELECTOR_LENGTH:
raise ValueError("Selector too long")
if selector_string.count(',') > MAX_SELECTOR_COUNT:
raise ValueError("Too many selectors")
return soup.select(selector_string)
```

  1. Set resource limits: In production, use memory limits via containers (--memory in Docker), cgroups, or resource.setrlimit() in Python to prevent any single process from exhausting system memory.

  2. Pin and audit lock files: The uv.lock file pins exact versions with cryptographic hashes. Regularly audit these pins against vulnerability databases.

  3. Defense in depth: Even with the library fix, don't rely solely on upstream patches. Layer input validation, resource limits, and monitoring.

Key Takeaways

  • Transitive dependencies like soupsieve can introduce high-severity vulnerabilities even when your direct code is secure—always scan the full dependency tree.
  • CSS selector parsing is a non-obvious attack surface: developers rarely think of soup.select() as a security-sensitive operation, but any parser handling unbounded input is a potential DoS vector.
  • The backend/uv.lock file is production code: lock files define exactly what runs in production, and a vulnerable version pinned there means the vulnerability ships.
  • A 288-byte increase in package size was all it took to add proper bounds checking—showing that security fixes don't need to be invasive.
  • CLI tools processing untrusted input files are still exploitable: the "local only" threat model doesn't eliminate risk when input files come from external sources.

How Orbis AppSec Detected This

  • Source: User-controlled input files or command-line arguments containing CSS selector strings that reach soupsieve's parsing logic
  • Sink: soupsieve 2.8.3's internal selector list parser (invoked via BeautifulSoup's select() method) in the backend application
  • Missing control: No upper bound on memory allocation when parsing comma-separated CSS selector lists; no input validation on selector complexity
  • CWE: CWE-400 (Uncontrolled Resource Consumption)
  • Fix: Upgraded soupsieve from 2.8.3 to 2.8.4 in backend/uv.lock, which introduces internal limits on selector list parsing

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

CVE-2026-49476 is a reminder that denial-of-service vulnerabilities often hide in the parsing layers of our dependencies. Soupsieve's comma-separated selector list handling seemed innocuous until researchers demonstrated that unbounded input could exhaust system memory. The fix—a single version bump from 2.8.3 to 2.8.4—is trivial to apply but critical to deploy. If your Python project uses BeautifulSoup (and most web-scraping or HTML-processing projects do), check your soupsieve version today.

References

Frequently Asked Questions

What is memory exhaustion via large comma-separated selector lists?

It's a denial-of-service vulnerability where an attacker provides an extremely large CSS selector string with many comma-separated selectors, causing the soupsieve parser to allocate unbounded memory until the system runs out of resources and the application crashes.

How do you prevent memory exhaustion vulnerabilities in Python?

Implement input size limits on user-controlled data before passing it to parsers, keep dependencies updated, use resource limits (e.g., memory caps via ulimit or cgroups), and validate/sanitize CSS selector strings before processing them with libraries like soupsieve.

What CWE is memory exhaustion?

CWE-400 (Uncontrolled Resource Consumption), which covers scenarios where software does not properly limit the amount of resources allocated in response to input, allowing attackers to cause denial of service.

Is input length validation enough to prevent memory exhaustion in CSS parsing?

Input length validation helps but isn't always sufficient alone. The issue in soupsieve was specifically about how comma-separated lists were processed internally—even moderately-sized inputs could trigger disproportionate memory allocation due to the parsing algorithm's behavior. Library-level fixes like soupsieve 2.8.4 address the root cause.

Can static analysis detect memory exhaustion vulnerabilities?

Yes, tools like Trivy (which detected this CVE), Snyk, and Dependabot can identify known vulnerable dependency versions. However, detecting novel memory exhaustion patterns in custom code typically requires dynamic analysis, fuzzing, or specialized static analyzers that track resource allocation paths.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #4089

Related Articles

high

How ReDoS happens in Node.js path-to-regexp and how to fix it

CVE-2024-52798 is a Regular Expression Denial of Service (ReDoS) vulnerability in the `path-to-regexp` package's 0.1.x branch, which remains unpatched in that legacy line. Because `path-to-regexp` is a transitive dependency pulled in by `websocket-driver` and many other popular Node.js packages, any application that processes attacker-controlled URL paths through an affected version is at risk of catastrophic backtracking that can freeze the event loop. Upgrading `websocket-driver` to 0.7.5 — an

high

How Denial of Service via Crafted Long-Path Tar Archives Happens in Node.js and How to Fix It

CVE-2026-73566 is a Denial of Service vulnerability in node-tar that allows attackers to craft specially malformed tar archives with excessively long file paths to exhaust system resources and crash applications. The fix upgrades tar from version 7.5.19 to 7.5.21, which implements proper path length validation to prevent this attack vector.

high

How Denial of Service via Memory Exhaustion happens in Socket.IO Parser and how to fix it

CVE-2026-69185 is a high-severity Denial of Service vulnerability in the `socket.io-parser` package that allows attackers to exhaust server memory by sending specially crafted packets. The fix upgrades `socket.io-parser` from version 4.2.4 to 4.2.7 (and parallel branches to 3.4.5 and 3.3.6) in `client/package-lock.json`, closing the attack surface against malicious clients. This kind of memory-exhaustion flaw is particularly dangerous in real-time applications where the parser handles a continuo

high

How express-check-csurf-middleware-usage happens in JavaScript/Express and how to fix it

A high-severity CSRF vulnerability was identified in `tower_game/index.js` where the Express application lacked any Cross-Site Request Forgery protection middleware. Without CSRF validation, an attacker could craft malicious pages that trick authenticated users into submitting unwanted requests to the game server. The fix adds `csurf` middleware with cookie-based token storage in just four lines of code.

high

How Quadratic CPU Consumption Happens in js-yaml and How to Fix It

A high-severity denial-of-service vulnerability in js-yaml versions prior to 4.3.1 allowed attackers to craft malicious YAML documents with !!omap tags that triggered quadratic CPU consumption during parsing. This fix upgrades js-yaml from 4.1.1 to 4.3.1 using npm overrides, protecting applications from algorithmic complexity attacks that could freeze or crash Node.js services.

critical

How Denial of Service via Gzip Bomb happens in Node.js and how to fix it

CVE-2026-59873 is a critical Denial of Service vulnerability in node-tar versions prior to 7.5.19, where a maliciously crafted gzip bomb can exhaust server resources when extracting archives. The fix upgrades the `tar` dependency from version 7.5.15 to 7.5.21 in `package-lock.json` and pins the version via an `overrides` block in `package.json`. Any application that processes user-supplied tar archives is at risk of resource exhaustion, making this an urgent upgrade.