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

medium

How insecure update manifest parsing happens in C++ UpdateHelper.cpp and how to fix it

TrafficMonitor's software update mechanism in `UpdateHelper.cpp` fetched and parsed update manifests from remote servers without validating the version string or enforcing trusted download URLs, leaving users exposed to man-in-the-middle (MITM) attacks. An attacker on the same network could intercept the update channel and inject a malicious binary under a crafted version string or an HTTP download link pointing to attacker-controlled infrastructure. The fix adds strict version-string sanitizati

high

How integer overflow in malloc happens in C bipartite matching and how to fix it

A high-severity integer overflow vulnerability was discovered in the bipartite matching algorithm implementation where unchecked multiplication operations for memory allocation could wrap around, causing undersized buffer allocations and subsequent heap overflow. The fix replaces vulnerable `malloc(sizeof(int) * V)` patterns with safe `calloc(V, sizeof(int))` calls and adds proper bounds validation to prevent exploitation.

high

How integer truncation heap overflow happens in C++ UEFI ACPI parsing and how to fix it

A high-severity integer truncation vulnerability was discovered in `Mobility.Uefi.Acpi.cpp` where heap allocation sizes were stored in a 16-bit integer (`MO_UINT16`), causing silent truncation when the computed size exceeded 65535 bytes. This led to undersized heap allocations followed by out-of-bounds writes, exploitable by an attacker who can influence ACPI SRAT table contents in virtualized environments. The fix promotes the size variable to `MO_UINTN` (platform-native width) to prevent trunc

critical

How API key exposure in configuration files happens in TOML config and how to fix it

A critical security vulnerability in `commands/webperf.toml` allowed API keys to be hardcoded directly in configuration files, creating a credential exposure risk. The documentation on line 11 suggested developers could provide `CRUX_API_KEY` or `GOOGLE_API_KEY` directly in the config, which could lead to these sensitive credentials being committed to version control or exposed in logs. The fix updated the documentation to explicitly require environment variables and warn against hardcoding cred

high

How path traversal happens in Ruby YARD server and how to fix it

A high-severity path traversal vulnerability (CVE-2026-41493) in YARD versions prior to 0.9.42 allowed attackers to read arbitrary files from servers running `yard server`. This fix upgrades the yard gem from 0.9.26 to 0.9.42 in the Gemfile and Gemfile.lock, closing a dangerous information disclosure vector that could expose configuration files, credentials, and source code.

high

How buffer overflow via sprintf() happens in C networking code and how to fix it

A high-severity buffer overflow vulnerability was discovered in `profile.c` where `sprintf()` was used to format server addresses without any bounds checking. An attacker who could influence the `SERVER_BASE_PORT` value or trigger integer overflow in the port calculation could write beyond the `server_address` buffer. The fix replaces `sprintf()` with `snprintf()` using explicit buffer size limits at both call sites (lines 99 and 220).