Open WebUI: Any authenticated user can read other users' private notes via Socket.IO
Medium5.3CVE-2026-54022 · Published Jun 17, 2026 · updated Jul 20, 2026
### Summary The `ydoc:document:join` Socket.IO handler checks note ownership only when the `document_id` starts with `note:` (colon). However, the `YdocManager` storage layer normalizes all document IDs by replacing colons with underscores (`document_id.replace(":", "_")`). An attacker can join a document room using `note_<id>` (underscore) instead of `note:<id>` (colon), bypassing the authorization check entirely while accessing the same underlying Yjs document. The server then returns the full document state, leaking the victim's private note contents. ### Details The `ydoc:document:join` handler in `socket/main.py` (line 511) only performs authorization for document IDs matching the `note:` prefix: ```python @sio.on("ydoc:document:join") async def ydoc_document_join(sid, data): document_id = data["document_id"] if document_id.startswith("note:"): note_id = document_id.split(":")[1] note = Notes.get_note_by_id(note_id) # ... ownership and AccessGrants check ... # Returns early if user doesn't have access # If document_id does NOT start with "note:", execution continues # with no authorization check at all await YDOC_MANAG...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| open-webui PyPI | < 0.8.11 | 0.8.11 |
Details and references
### Summary The `ydoc:document:join` Socket.IO handler checks note ownership only when the `document_id` starts with `note:` (colon). However, the `YdocManager` storage layer normalizes all document IDs by replacing colons with underscores (`document_id.replace(":", "_")`). An attacker can join a document room using `note_<id>` (underscore) instead of `note:<id>` (colon), bypassing the authorization check entirely while accessing the same underlying Yjs document. The server then returns the full document state, leaking the victim's private note contents. ### Details The `ydoc:document:join` handler in `socket/main.py` (line 511) only performs authorization for document IDs matching the `note:` prefix: ```python @sio.on("ydoc:document:join") async def ydoc_document_join(sid, data): document_id = data["document_id"] if document_id.startswith("note:"): note_id = document_id.split(":")[1] note = Notes.get_note_by_id(note_id) # ... ownership and AccessGrants check ... # Returns early if user doesn't have access # If document_id does NOT start with "note:", execution continues # with no authorization check at all await YDOC_MANAGER.add_user(document_id=document_id, user_id=sid) await sio.enter_room(sid, f"doc_{document_id}") ydoc = Y.Doc() updates = await YDOC_MANAGER.get_updates(document_id) for update in updates: ydoc.apply_update(bytes(update)) state_update = ydoc.get_update() await sio.emit("ydoc:document:state", { "document_id": document_id, "state": list(state_update), }, room=sid) ``` The `YdocManager` class in `socket/utils.py` normalizes document IDs in every method by replacing colons with underscores: ```python async def get_updates(self, document_id: str) -> List[bytes]: document_id = document_id.replace(":", "_") # line 176 # ... returns updates keyed by normalized ID async def append_to_updates(self, document_id: str, update: bytes): document_id = document_id.replace(":", "_") # line 134 # ... stores update keyed by normalized ID ``` This means `note:abc123` and `note_abc123` resolve to the same storage key (`note_abc123`). When a victim opens their note, the Yjs document is stored under the normalized key. An attacker can then request the same document using the underscore variant, which skips the `startswith("note:")` authorization check but retrieves the same data from `YdocManager`. ### PoC ```python #!/usr/bin/env python3 """ uv run --no-project --with requests --with "python-socketio[asyncio_client]" --with aiohttp --with pycrdt finding_15_yjs_note_disclosure.py --base-url BASE_URL --attacker-email EMAIL --attacker-password PASS --victim-email EMAIL --victim-password PASS Finding #15 , Any authenticated user can read other users' private notes via Socket.IO SUMMARY: The ydoc:document:join Socket.IO handler only checks authorization for document IDs starting with "note:" (colon). However, YdocManager normalizes document IDs by replacing colons with underscores internally. An attacker can join a room using "note_<id>" (underscore) to bypass the auth check, while still accessing the same underlying Yjs document as "note:<id>". Then ydoc:document:state returns the full document content. VULNERABLE CODE: backend/open_webui/socket/main.py, ydoc:document:join: if document_id.startswith("note:"): # permission check only for colon-prefix # "note_<id>" skips this check entirely backend/open_webui/socket/ydoc.py, YdocManager: key = document_id.replace(":", "_") # normalizes to same storage key IMPACT: Any authenticated user can read the full content of any other user's notes by exploiting the namespace collision between "note:" and "note_" prefixes. REPRODUCTION: 1. Victim creates a private note with sensitive content. 2. Attacker connects via Socket.IO and authenticates. 3. Attacker joins room with document_id "note_<victim_note_id>" (
More Open WebUI advisories
All Open WebUI| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Jun 17 | Open WebUI: improper access control | Medium6.3 | 0.9.6 |
| Jun 17 | Open WebUI: RAG ACL Bypass in Milvus Multitenancy Mode | Medium6.5 | 0.9.6 |
| Jun 17 | Open WebUI: SSRF Protection Bypass in Playwright Web Loader via HTTP Redirects | High7.7 | 0.9.6 |
| Jun 17 | Open WebUI: Path traversal / SSRF in terminal server proxy via encoded path traversal | High7.7 | 0.9.6 |
| Jun 17 | Open WebUI BOLA: `search_knowledge_files` Allows Unauthorized Knowledge Base File Enumeration | Medium4.3 | 0.9.6 |
| Jun 17 | Open WebUI Prompt history IDOR: unbound history_id allows cross-prompt read and deletion | Medium6.4 | 0.9.6 |