높음취약점
Open WebUI: Stale Admin Role in Socket.IO Session Pool Enables Post-Demotion Cross-User Note Access
GHSA-45m8-cpm2-3v65 · CVE-2026-44553 · PYSEC-2026-2697 · 갱신 2026. 7. 14.
영향 받는 패키지
| 생태계 | 패키지 | 수정 버전 |
|---|---|---|
| PyPI | open-webui | 0.9.0 |
GHSA-45m8-cpm2-3v65 · CVE-2026-44553 · PYSEC-2026-2697 · 갱신 2026. 7. 14.
| 생태계 | 패키지 | 수정 버전 |
|---|---|---|
| PyPI | open-webui | 0.9.0 |
HTTP endpoints are not affected — get_current_user at utils/auth.py refetches the user record from the database on every request. The gap is exclusive to the Socket.IO session cache.
# socket/main.py:330-351 — role snapshotted at connect time
async def connect(sid, environ, auth):
user = None
if auth and 'token' in auth:
data = decode_token(auth['token'])
if data is not None and 'id' in data:
user = Users.get_user_by_id(data['id'])
if user:
SESSION_POOL[sid] = {
'id': user.id,
'role': user.role, # ← snapshotted, never refreshed
...
}
# socket/main.py:393-398 — heartbeat refreshes last_seen_at only
async def heartbeat(sid, data):
user = SESSION_POOL.get(sid)
if user:
SESSION_POOL[sid] = {**user, 'last_seen_at': int(time.time())}
# role is carried forward unchanged
# socket/main.py:538 — admin check against cached role
if user.get('role') != 'admin' and not has_access(user_id, 'note', note_id, 'read', db=db):
return
SESSION_POOL[sid] records role='admin'.POST /api/v1/users/{B_id}/update. The DB user.role becomes 'user'.heartbeat events every few seconds; these are accepted and only refresh last_seen_at.ydoc:document:join with document_id = 'note:<victim_note_id>' for any note they do not own.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.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.The 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.