Skip to content

Commit ded3bad

Browse files
1 parent e11540b commit ded3bad

4 files changed

Lines changed: 246 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-3x8w-4f7p-xxc2",
4+
"modified": "2026-05-08T19:44:40Z",
5+
"published": "2026-05-08T19:44:40Z",
6+
"aliases": [
7+
"CVE-2026-44552"
8+
],
9+
"summary": "Open WebUI: Redis Cache Keys tool_servers and terminal_servers Missing Instance Prefix Enable Cross-Instance Cache Poisoning",
10+
"details": "# Redis Cache Keys tool_servers and terminal_servers Missing Instance Prefix Enable Cross-Instance Cache Poisoning\n\n## Affected Component\n\nTool server and terminal server Redis cache:\n- `backend/open_webui/utils/tools.py` (line 841, tool_servers SET)\n- `backend/open_webui/utils/tools.py` (line 850, tool_servers GET)\n- `backend/open_webui/utils/tools.py` (line 976, terminal_servers SET)\n- `backend/open_webui/utils/tools.py` (line 986, terminal_servers GET)\n\n## Affected Versions\n\nCurrent main branch (commit `6fdd19bf1`) and likely all versions since the tool server / terminal server Redis cache was introduced.\n\n## Description\n\nOpen WebUI uses a `REDIS_KEY_PREFIX` (default `open-webui`) to namespace Redis keys, allowing multiple instances to safely share a single Redis backend. Every Redis key in the codebase uses this prefix — except the `tool_servers` and `terminal_servers` keys in `utils/tools.py`, which use bare key names.\n\nWhen two or more Open WebUI instances share a Redis database (a supported and documented deployment pattern, e.g., for multi-region deployments, blue-green setups, or cluster topologies), the unprefixed keys collide. An admin on Instance A writing to `tool_servers` overwrites the value read by Instance B — causing Instance B's users to receive Instance A's tool server configuration.\n\n```python\n# utils/tools.py — unprefixed keys (problem)\nawait request.app.state.redis.set('tool_servers', ...) # line 841\njson.loads(await request.app.state.redis.get('tool_servers')) # line 850\nawait request.app.state.redis.set('terminal_servers', ...) # line 976\njson.loads(await request.app.state.redis.get('terminal_servers')) # line 986\n\n# Every other Redis key in the codebase — prefixed (correct pattern)\nf'{REDIS_KEY_PREFIX}:auth:token:{jti}:revoked'\nf'{REDIS_KEY_PREFIX}:ratelimit:{email}:{bucket}'\nf'{REDIS_KEY_PREFIX}:tasks:commands'\n```\n\n## Attack Scenario\n\nTwo Open WebUI instances (A and B) share a Redis backend — a supported deployment for multi-region setups, blue-green deployments, or hot-standby. Both instances have their own admin accounts; the shared Redis was chosen for coordinated session handling, rate limiting, and task management.\n\n1. Attacker is an admin on Instance A (a legitimately provisioned admin, or one that escalated via any available path including the LDAP empty-password or stale-admin-role findings).\n2. Attacker on Instance A configures a tool server pointing to `https://attacker-controlled.example.com/openapi.json`. This triggers `utils/tools.py:841` to write the new tool server list under the bare key `tool_servers`.\n3. Instance B's users query tools. Instance B reads from `tool_servers` (line 850) — gets Instance A's poisoned list, which now includes the attacker's server alongside or instead of Instance B's legitimate tool servers.\n4. Instance B's users invoke tools through the model's context. The attacker's server receives tool call payloads containing: chat content, user identity, OAuth tokens scoped to the tool server (if the user has bound their external account), and in-flight conversation context.\n5. The attacker's server returns arbitrary tool responses, which are fed back into Instance B's LLM context as \"trusted tool output\" — enabling prompt injection, misinformation delivery, and further data exfiltration cascades.\n\nThe same cross-instance poisoning applies to `terminal_servers`.\n\n## Impact\n\n- Cross-instance cache poisoning: an admin on one instance affects all users of another instance sharing the Redis backend\n- Data exfiltration: tool call payloads contain chat content and user identity, delivered to the attacker's server\n- Prompt injection delivery: attacker-returned tool responses enter the victim instance's LLM context as trusted data\n- Undermines the multi-instance isolation guarantee that `REDIS_KEY_PREFIX` was introduced to provide\n- Silent failure mode: no error is raised; the victim instance sees a valid, signed cache entry and has no way to detect it came from a different instance\n\n## Preconditions\n\n- Multiple Open WebUI instances share a single Redis backend (a supported and documented deployment)\n- Attacker has admin access on one of the instances (or escalates to admin via any available path)",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "open-webui"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "0.9.0"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 0.8.12"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/open-webui/open-webui/security/advisories/GHSA-3x8w-4f7p-xxc2"
45+
},
46+
{
47+
"type": "PACKAGE",
48+
"url": "https://github.com/open-webui/open-webui"
49+
}
50+
],
51+
"database_specific": {
52+
"cwe_ids": [
53+
"CWE-668"
54+
],
55+
"severity": "HIGH",
56+
"github_reviewed": true,
57+
"github_reviewed_at": "2026-05-08T19:44:40Z",
58+
"nvd_published_at": null
59+
}
60+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-45m8-cpm2-3v65",
4+
"modified": "2026-05-08T19:43:49Z",
5+
"published": "2026-05-08T19:43:49Z",
6+
"aliases": [
7+
"CVE-2026-44553"
8+
],
9+
"summary": "Open WebUI: Stale Admin Role in Socket.IO Session Pool Enables Post-Demotion Cross-User Note Access",
10+
"details": "# Stale Admin Role in Socket.IO Session Pool Enables Post-Demotion Cross-User Note Access\n\n## Affected Component\n\nSocket.IO session state and role-check callsites:\n- `backend/open_webui/socket/main.py` (lines 330-351, `connect` handler — role snapshotted into SESSION_POOL)\n- `backend/open_webui/socket/main.py` (lines 393-398, `heartbeat` handler — does not refresh role)\n- `backend/open_webui/socket/main.py` (line 538, `ydoc:document:join` — uses cached role for admin check)\n- `backend/open_webui/socket/main.py` (line 611, `document_save_handler` — uses cached role for admin check)\n- `backend/open_webui/routers/users.py` (lines 557-633, role update — does not invalidate SESSION_POOL)\n- `backend/open_webui/routers/users.py` (line 641, user delete — does not invalidate SESSION_POOL)\n\n## Affected Versions\n\nCurrent main branch (commit `6fdd19bf1`) and likely all versions with the collaborative document (Yjs) Socket.IO handlers.\n\n## Description\n\nWhen a user connects via Socket.IO, the `connect` handler authenticates them via JWT and stores their user record (including `role`) in the in-memory `SESSION_POOL` dictionary keyed by session ID. The `heartbeat` handler keeps the session alive indefinitely but only refreshes the `last_seen_at` timestamp — never the role.\n\nRole checks in the Yjs collaborative document handlers (`ydoc:document:join`, `document_save_handler`) consult the cached `SESSION_POOL` role rather than the database. Meanwhile, administrative role changes and user deletions do not iterate `SESSION_POOL` to disconnect affected sessions. As a result, a user whose admin role has been revoked retains admin privileges within their existing Socket.IO session for as long as they keep the connection alive (via automatic heartbeats).\n\nHTTP endpoints are not affected — `get_current_user` at [utils/auth.py](backend/open_webui/utils/auth.py) refetches the user record from the database on every request. The gap is exclusive to the Socket.IO session cache.\n\n```python\n# socket/main.py:330-351 — role snapshotted at connect time\nasync def connect(sid, environ, auth):\n user = None\n if auth and 'token' in auth:\n data = decode_token(auth['token'])\n if data is not None and 'id' in data:\n user = Users.get_user_by_id(data['id'])\n if user:\n SESSION_POOL[sid] = {\n 'id': user.id,\n 'role': user.role, # ← snapshotted, never refreshed\n ...\n }\n\n# socket/main.py:393-398 — heartbeat refreshes last_seen_at only\nasync def heartbeat(sid, data):\n user = SESSION_POOL.get(sid)\n if user:\n SESSION_POOL[sid] = {**user, 'last_seen_at': int(time.time())}\n # role is carried forward unchanged\n\n# socket/main.py:538 — admin check against cached role\nif user.get('role') != 'admin' and not has_access(user_id, 'note', note_id, 'read', db=db):\n return\n```\n\n## Attack Scenario\n\n1. User B is an admin and has an active browser session with a live Socket.IO connection. `SESSION_POOL[sid]` records `role='admin'`.\n2. Admin A demotes User B to a regular user via `POST /api/v1/users/{B_id}/update`. The DB `user.role` becomes `'user'`.\n3. No Socket.IO disconnect, no SESSION_POOL update, no token revocation event is triggered by the role change.\n4. User B's client continues sending `heartbeat` events every few seconds; these are accepted and only refresh `last_seen_at`.\n5. User B emits `ydoc:document:join` with `document_id = 'note:<victim_note_id>'` for any note they do not own.\n6. The handler at line 538 evaluates `user.get('role') != 'admin'` — returns `False` because `SESSION_POOL` still holds the stale `admin` role. Access check is bypassed, User B joins the document room, receives full document state and live updates.\n7. User B emits `ydoc:document:update` for the same note. The handler at line 611 performs the same cached-admin check, bypasses authorization, and persists attacker-controlled content to the victim's note via `Notes.update_note_by_id`.\n\nThe same bypass occurs if the user is deleted entirely (`delete_user_by_id`) — the deleted user retains admin privileges on their live socket until disconnection.\n\n## Impact\n\n- Read access to any user's notes after admin privileges have been revoked\n- Write access (content injection, overwrite) to any user's notes under the same conditions\n- The stale privilege is bounded only by the attacker's willingness to keep the Socket.IO connection alive; heartbeats extend the session indefinitely\n- Official admin demotion or user deletion gives a false sense of security — HTTP access is correctly revoked, but real-time collaborative access silently continues\n\n## Preconditions\n\n- Attacker must have an active Socket.IO connection established while they held admin role\n- Attacker must retain the Socket.IO session after demotion/deletion (trivial — just don't close the browser)",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "open-webui"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "0.9.0"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 0.8.12"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/open-webui/open-webui/security/advisories/GHSA-45m8-cpm2-3v65"
45+
},
46+
{
47+
"type": "PACKAGE",
48+
"url": "https://github.com/open-webui/open-webui"
49+
}
50+
],
51+
"database_specific": {
52+
"cwe_ids": [
53+
"CWE-384",
54+
"CWE-863"
55+
],
56+
"severity": "HIGH",
57+
"github_reviewed": true,
58+
"github_reviewed_at": "2026-05-08T19:43:49Z",
59+
"nvd_published_at": null
60+
}
61+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-9vvh-qmjx-p4q8",
4+
"modified": "2026-05-08T19:45:03Z",
5+
"published": "2026-05-08T19:45:03Z",
6+
"aliases": [
7+
"CVE-2026-44555"
8+
],
9+
"summary": "Open WebUI's Base Model Routing Bypasses Access Control via Model Chaining",
10+
"details": "# Base Model Routing Bypasses Access Control via Model Chaining\n\n## Affected Component\n\nModel chaining via `base_model_id`:\n- `backend/open_webui/routers/models.py` (lines 170-214, `create_new_model`)\n- `backend/open_webui/routers/models.py` (lines 254-308, `import_models`)\n- `backend/open_webui/main.py` (lines 1696-1711, base model resolution in chat completion)\n- `backend/open_webui/routers/openai.py` (lines 1032-1037, base model payload rewrite)\n- `backend/open_webui/routers/ollama.py` (lines 1086-1090, base model payload rewrite)\n- `backend/open_webui/utils/models.py` (line 380, `check_model_access` — checks user-facing model only)\n\n## Affected Versions\n\nCurrent main branch (commit `6fdd19bf1`) and likely all versions with the model chaining (`base_model_id`) feature.\n\n## Description\n\nOpen WebUI supports model composition via `base_model_id`: a user-defined model (e.g., \"Cheap Assistant\") can reference an existing base model (e.g., \"gpt-4-turbo-restricted\") that provides the actual inference capability. When a user queries the composed model, the access control pipeline verifies the user has access to the composed model but never re-verifies access to the chained base model.\n\nAdditionally, the model creation and import endpoints accept arbitrary `base_model_id` values without checking that the caller has access to that base model. Combined, this allows any user with the default model creation permission to create a model that chains to a restricted base model — and then invoke it, causing the server to dispatch the request to the restricted base model using the admin-configured API key.\n\n```python\n# utils/models.py:380 — access check runs against the user-facing model only\ndef check_model_access(user, model):\n if user.role == 'user':\n ...check access grants on `model`...\n\n# main.py:1696-1711 — base model resolved without access check\nbase_model = request.app.state.MODELS.get(model.info.base_model_id)\nif base_model:\n # payload[\"model\"] is rewritten to base_model.id\n # but no check_model_access(user, base_model) is performed\n\n# openai.py:1032-1037 / ollama.py:1086-1090 — the rewritten payload is dispatched\npayload['model'] = base_model_id\n```\n\n## Attack Scenario\n\n1. Admin provisions a premium/restricted model `gpt-4-turbo-restricted` and configures access grants so only the \"ML Engineers\" group can use it.\n2. Attacker (a regular user not in that group) calls:\n ```\n POST /api/v1/models/create\n {\n \"id\": \"cheap-assistant\",\n \"name\": \"Cheap Assistant\",\n \"base_model_id\": \"gpt-4-turbo-restricted\",\n \"params\": {},\n \"meta\": {}\n }\n ```\n The creation endpoint does not validate the attacker's access to `gpt-4-turbo-restricted`.\n3. Attacker now owns `cheap-assistant`. `check_model_access(attacker, cheap-assistant)` passes trivially because they are the owner.\n4. Attacker sends:\n ```\n POST /api/chat/completions\n {\"model\": \"cheap-assistant\", \"messages\": [...]}\n ```\n5. At `main.py:1696`, the pipeline resolves `cheap-assistant.base_model_id` to `gpt-4-turbo-restricted`, rewrites `payload[\"model\"]` to the base model ID, and dispatches the upstream request with the admin-configured API key for the backend.\n6. The attacker receives responses from the restricted model, bypassing the access grant policy.\n\nThe same bypass is available via the import endpoint, which additionally allows overwriting existing models (see related finding on model import ownership).\n\n## Impact\n\n- Regular users can query restricted models by chaining through a self-owned wrapper model\n- Access control on `gpt-4-turbo-restricted` (or equivalent paid/tiered/internal models) becomes silently ineffective\n- Direct cost impact on pay-per-token backends (OpenAI, Anthropic, Azure) — the admin's API key is used for requests the admin intended to forbid\n- Creates a false sense of security — the admin sees access restrictions work through the standard model selector but not through user-created chains\n\n## Preconditions\n\n- Attacker must have model creation permission (default `workspace.models` permission, granted to all users by default)\n- A restricted base model must exist on the instance (the target of the chain)",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "open-webui"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "0.9.0"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 0.8.12"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/open-webui/open-webui/security/advisories/GHSA-9vvh-qmjx-p4q8"
45+
},
46+
{
47+
"type": "PACKAGE",
48+
"url": "https://github.com/open-webui/open-webui"
49+
}
50+
],
51+
"database_specific": {
52+
"cwe_ids": [
53+
"CWE-862"
54+
],
55+
"severity": "HIGH",
56+
"github_reviewed": true,
57+
"github_reviewed_at": "2026-05-08T19:45:03Z",
58+
"nvd_published_at": null
59+
}
60+
}

0 commit comments

Comments
 (0)