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 Quadratic CPU Consumption Vulnerabilities Happen in JavaScript YAML Parsers and How to Fix Them

A high-severity denial-of-service vulnerability in js-yaml versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through specially crafted YAML documents using the !!omap tag. This fix upgrades js-yaml from 4.1.1 to 4.3.1 and from 3.14.2 to 3.15.1, eliminating the algorithmic complexity attack vector that could freeze Node.js applications processing untrusted YAML input.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `scripts/build.js` where `execSync` was called with string-interpolated arguments (`sourceDir` and `outputPath`) inside a shell command. By replacing `execSync` with `spawnSync` using an argument array (no shell), the fix eliminates the possibility of shell metacharacter injection while preserving identical build behavior.

high

How Command Injection happens in Node.js child_process and how to fix it

A command injection vulnerability in nix.js's Release class allowed potentially malicious input through the `arch` parameter to be executed via shell commands. The fix replaced `execSync()` with `execFileSync()`, eliminating shell interpretation and preventing command injection by passing arguments as an array instead of a concatenated string.

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.

critical

How HTTP Header Injection Happens in Go and How to Fix It

A critical vulnerability in the file upload handler allowed attackers to inject CRLF sequences into HTTP response headers through crafted filenames. The fix sanitizes user-supplied filenames before using them in Content-Disposition headers, preventing header injection attacks that could lead to cache poisoning, session fixation, or XSS.

high

How Path Traversal and Security Policy Bypass Happens in Node.js Dependencies and How to Fix It

A high-severity vulnerability in the fast-uri package (CVE-2026-6321) allowed attackers to bypass security policies through improper Unicode hostname canonicalization and path traversal. This issue affected the @apralabs/apra-fleet project through its dependency tree, and was resolved by upgrading fast-uri from version 3.1.0 to 4.1.2 using npm overrides.