Open WebUI: Any authenticated user can read other users' private notes via Socket.IO
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...
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>" (