Introduction
In a backend Python application, our security scanner flagged a high-severity vulnerability in the backend/uv.lock file—specifically, an outdated version of the soupsieve library that's susceptible to memory exhaustion attacks. The pinned version 2.8.3 contains CVE-2026-49476, which allows attackers to craft malicious CSS selector strings that cause unbounded memory allocation during parsing.
This matters particularly for this codebase because, as noted in the threat model, this is a CLI tool where attackers could potentially control input files. If an attacker supplies a crafted HTML file with malicious CSS selectors, they could crash the application or exhaust system resources on the machine running the tool.
The Vulnerability Explained
Soup Sieve is a CSS selector library for Beautiful Soup, widely used in Python web scraping and HTML processing applications. When parsing CSS selectors, the library must handle comma-separated lists like div, span, p, a where each comma introduces a new selector to match.
How the Memory Exhaustion Works
In soupsieve 2.8.3, the selector parser doesn't impose adequate limits on how many selectors can appear in a comma-separated list. Consider what happens when parsing a selector like:
a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a...
Repeated thousands or millions of times, each selector in the list requires memory allocation for:
- The parsed selector object
- Internal data structures for matching logic
- Cached compilation results
Without bounds checking, an attacker can force the parser to allocate memory proportional to the number of selectors provided—potentially gigabytes of RAM from a relatively small input string.
Attack Scenario Specific to This Application
Since this is a CLI tool that processes input files, an attacker could:
- Create a malicious HTML file containing a
<style>block with an enormous comma-separated selector list - Provide this file as input to the CLI tool
- When the application parses the HTML using Beautiful Soup with soupsieve, memory consumption spikes
- The host machine becomes unresponsive or the application crashes with an out-of-memory error
Even if the attacker doesn't have direct access to the machine, if the CLI tool processes files from untrusted sources (downloaded content, user uploads, etc.), the attack surface exists.
The Fix
The fix is elegantly simple—upgrade the vulnerable dependency to a patched version. Here's the exact change made 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, upload-time = "2026-01-20T04:27:02.457Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
]
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, upload-time = "2026-05-24T13:55:57.154Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" },
]
What Changed in soupsieve 2.8.4
The patched version (2.8.4) introduces:
- Selector count limits: The parser now enforces a maximum number of selectors in comma-separated lists
- Memory allocation bounds: Internal data structures have size caps to prevent runaway allocation
- Early termination: Parsing stops and raises an exception when limits are exceeded, rather than attempting to process unbounded input
Notice the package size increased from 37,016 bytes to 37,304 bytes—the additional ~300 bytes represent the new validation and limit-checking code.
Prevention & Best Practices
1. Keep Dependencies Updated
This vulnerability was in a transitive dependency—soupsieve is typically pulled in by Beautiful Soup. Regularly audit and update your dependency tree:
# For uv-managed projects
uv lock --upgrade-package soupsieve
# For pip-tools
pip-compile --upgrade-package soupsieve
# For poetry
poetry update soupsieve
2. Use Vulnerability Scanners in CI/CD
Integrate tools like Trivy, Snyk, or Dependabot into your pipeline:
# Example GitHub Actions workflow
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'HIGH,CRITICAL'
3. Implement Defense in Depth for CLI Tools
Even with patched libraries, add additional protections:
import resource
# Limit memory usage to 512MB
resource.setrlimit(resource.RLIMIT_AS, (512 * 1024 * 1024, 512 * 1024 * 1024))
# Now parse potentially untrusted content
from bs4 import BeautifulSoup
soup = BeautifulSoup(untrusted_html, 'html.parser')
4. Validate Input Before Parsing
For CLI tools processing external files:
import os
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
def safe_parse_html(filepath):
file_size = os.path.getsize(filepath)
if file_size > MAX_FILE_SIZE:
raise ValueError(f"File too large: {file_size} bytes")
with open(filepath, 'r') as f:
return BeautifulSoup(f.read(), 'html.parser')
Key Takeaways
- Lock files need security audits too: The vulnerability wasn't in application code but in
backend/uv.lock—dependency manifests are attack surface - Transitive dependencies matter: soupsieve is typically a dependency of Beautiful Soup, not directly declared—audit your full dependency tree
- CLI tools aren't exempt from DoS: Even local tools can be attacked through crafted input files
- Minor version bumps can contain critical fixes: 2.8.3 → 2.8.4 is a patch release but addresses a high-severity CVE
- Verify hashes after upgrades: The lock file includes SHA256 hashes—always verify these match PyPI's published values
How Orbis AppSec Detected This
- Source: User-controlled input files processed by the CLI tool, potentially containing CSS selectors
- Sink: soupsieve's selector parser (invoked through Beautiful Soup's CSS selector methods)
- Missing control: The pinned soupsieve version 2.8.3 lacked resource limits on selector list parsing
- CWE: CWE-400 (Uncontrolled Resource Consumption)
- Fix: Upgraded soupsieve from 2.8.3 to 2.8.4 in
backend/uv.lock, which implements proper memory bounds
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 demonstrates that even well-established libraries can contain resource exhaustion vulnerabilities that slip past initial review. The fix was straightforward—a single version bump in the lock file—but the consequences of leaving it unpatched could range from application crashes to complete system unavailability.
For Python developers working with HTML parsing, this serves as a reminder to:
1. Regularly scan dependencies for known vulnerabilities
2. Treat lock files as security-critical configuration
3. Implement resource limits when processing untrusted input
4. Stay informed about CVEs affecting your dependency tree
Security is a continuous process, and automated tools can help catch issues like this before they reach production.