Require auth on ydoc:awareness:update and ydoc:document:leave handlers (CWE-306) (#25946)

These were the only two ydoc Socket.IO handlers missing the SESSION_POOL guard that
every sibling (join/state/update) enforces. With always_connect=True admitting
unauthenticated sockets, a client that knew a note's document_id could broadcast
spoofed awareness (cursors/presence) and fake ydoc:user:left events into a live
editing room, attributed to any client-supplied user_id.

Both handlers now require an authenticated SESSION_POOL session; the awareness handler
additionally requires prior ydoc:document:join (room membership); and the broadcast
user_id is fixed to the authenticated identity instead of the client-supplied value,
removing the impersonation. Document content was never reachable (ydoc:document:update
already enforces write permission).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Classic298andClaude Opus 4.8 authored and GitHub committed 2026-06-17 00:06:13 +02:00
1 parent 993e749121
commit 22f2fe1ffb
1 file changed
+16 -9
+16 -9
View File
@@ -757,11 +757,13 @@ async def yjs_document_update(sid, data):
@sio.on('ydoc:document:leave') @sio.on('ydoc:document:leave')
async def yjs_document_leave(sid, data): async def yjs_document_leave(sid, data):
"""Handle user leaving a document""" """Handle user leaving a document"""
user = SESSION_POOL.get(sid)
if not user: # authenticated session required (parity with sibling handlers)
return
try: try:
document_id = normalize_document_id(data['document_id']) document_id = normalize_document_id(data['document_id'])
user_id = data.get('user_id', sid)
log.info(f'User {user_id} leaving document {document_id}') log.info(f'User {user["id"]} leaving document {document_id}')
# Remove user from the document # Remove user from the document
await YDOC_MANAGER.remove_user(document_id=document_id, user_id=sid) await YDOC_MANAGER.remove_user(document_id=document_id, user_id=sid)
@@ -769,10 +771,10 @@ async def yjs_document_leave(sid, data):
# Leave Socket.IO room # Leave Socket.IO room
await sio.leave_room(sid, f'doc_{document_id}') await sio.leave_room(sid, f'doc_{document_id}')
# Notify other users # Notify other users; user_id is the authenticated identity, not client-supplied
await sio.emit( await sio.emit(
'ydoc:user:left', 'ydoc:user:left',
{'document_id': document_id, 'user_id': user_id}, {'document_id': document_id, 'user_id': user['id']},
room=f'doc_{document_id}', room=f'doc_{document_id}',
) )
@@ -787,16 +789,21 @@ async def yjs_document_leave(sid, data):
@sio.on('ydoc:awareness:update') @sio.on('ydoc:awareness:update')
async def yjs_awareness_update(sid, data): async def yjs_awareness_update(sid, data):
"""Handle awareness updates (cursors, selections, etc.)""" """Handle awareness updates (cursors, selections, etc.)"""
user = SESSION_POOL.get(sid)
if not user: # authenticated session required (parity with sibling handlers)
return
try: try:
document_id = data['document_id'] document_id = normalize_document_id(data['document_id'])
user_id = data.get('user_id', sid) room = f'doc_{document_id}'
if room not in sio.rooms(sid): # must have joined the document first
return
update = data['update'] update = data['update']
# Broadcast awareness update to all other users in the document # Broadcast to the room; user_id is the authenticated identity, not client-supplied
await sio.emit( await sio.emit(
'ydoc:awareness:update', 'ydoc:awareness:update',
{'document_id': document_id, 'user_id': user_id, 'update': update}, {'document_id': document_id, 'user_id': user['id'], 'update': update},
room=f'doc_{document_id}', room=room,
skip_sid=sid, skip_sid=sid,
) )