Skip to content
Open WebUIGHSA-f3g7-59qc-pqg6

Open WebUI IDOR: Calendar event re-parenting allows writing events into another user's calendar

Medium4.3CVE-2026-54006 · Published Jun 17, 2026 · updated Jul 20, 2026

### Summary `POST /api/v1/calendars/events/{event_id}/update` validates that the caller has **write** access to the calendar the event *currently* belongs to, but does not validate the **destination** `calendar_id` supplied in the request body. The model layer then persists the new `calendar_id` unconditionally. A regular `user`-role account can therefore create an event in their own calendar and immediately move it into any other user's calendar whose ID they know , bypassing the authorization check that `create_event` correctly performs. This is reachable on **default configuration**: `ENABLE_CALENDAR` and `USER_PERMISSIONS_FEATURES_CALENDAR` both default to `True`. ### Details ### Sink , missing destination check `backend/open_webui/routers/calendar.py:283-297` ```python @router.post('/events/{event_id}/update', response_model=CalendarEventModel) async def update_event( request: Request, event_id: str, form_data: CalendarEventUpdateForm, user: UserModel = Depends(get_verified_user) ): await check_calendar_permission(request, user) event = await CalendarEvents.get_event_by_id(event_id) if not event: raise HTTPException(status_code=404, detail='E...

GitHub advisory

Affected versions

PackageAffectedFixed in
open-webui
PyPI
< 0.9.60.9.6
Details and references

### Summary `POST /api/v1/calendars/events/{event_id}/update` validates that the caller has **write** access to the calendar the event *currently* belongs to, but does not validate the **destination** `calendar_id` supplied in the request body. The model layer then persists the new `calendar_id` unconditionally. A regular `user`-role account can therefore create an event in their own calendar and immediately move it into any other user's calendar whose ID they know , bypassing the authorization check that `create_event` correctly performs. This is reachable on **default configuration**: `ENABLE_CALENDAR` and `USER_PERMISSIONS_FEATURES_CALENDAR` both default to `True`. ### Details ### Sink , missing destination check `backend/open_webui/routers/calendar.py:283-297` ```python @router.post('/events/{event_id}/update', response_model=CalendarEventModel) async def update_event( request: Request, event_id: str, form_data: CalendarEventUpdateForm, user: UserModel = Depends(get_verified_user) ): await check_calendar_permission(request, user) event = await CalendarEvents.get_event_by_id(event_id) if not event: raise HTTPException(status_code=404, detail='Event not found') await _check_calendar_access(event.calendar_id, user, 'write') # ← SOURCE only updated = await CalendarEvents.update_event_by_id(event_id, form_data) # ← writes form_data.calendar_id ... ``` `backend/open_webui/models/calendar.py:658-693` (`update_event_by_id`) ```python update_data = form_data.model_dump(exclude_unset=True) for field in [ 'calendar_id', # ← destination persisted with no ACL 'title', 'description', 'start_at', 'end_at', 'all_day', 'rrule', 'color', 'location', 'is_cancelled', ]: if field in update_data: setattr(event, field, update_data[field]) ``` ### Reference , `create_event` does check the destination `backend/open_webui/routers/calendar.py:255` ```python await _check_calendar_access(form_data.calendar_id, user, 'write') ``` ### Default-config gates (both `True`) - `backend/open_webui/config.py:1658-1662` , `ENABLE_CALENDAR` defaults `'True'` - `backend/open_webui/config.py:1554` , `USER_PERMISSIONS_FEATURES_CALENDAR` defaults `'True'` - `backend/open_webui/main.py:1457` , router mounted unconditionally ### PoC Verified end-to-end against the official `ghcr.io/open-webui/open-webui:main` (v0.9.4) Docker image with two fresh `user`-role accounts. #### 1. Environment ```bash git clone https://github.com/open-webui/open-webui.git cd open-webui && docker compose up -d # http://localhost:3000 ``` Create the first account (admin), then via admin UI / `POST /api/v1/auths/add` create two `user`-role accounts: **attacker** and **victim**. Sign each in and capture their JWTs as `$ATTACKER_TOKEN` / `$VICTIM_TOKEN`. #### 2. Obtain the victim's `calendar_id` Calendar IDs are UUIDv4 (`models/calendar.py:316`) and not enumerable. In practice an attacker obtains one via: - **Read-only share** , victim (or a group admin) grants the attacker `read` on a calendar; the ID is returned by `GET /api/v1/calendars/`. - **Event invitation** , victim adds the attacker as an attendee on any event; the event payload (`CalendarEventModel`, `models/calendar.py:127`) includes `calendar_id`. - Any side-channel (logs, screenshots, browser history). For reproduction the maintainer can simply read it as the victim: ```bash VICTIM_CALENDAR_ID=$(curl -s "$OPENWEBUI/api/v1/calendars/" \ -H "Authorization: Bearer $VICTIM_TOKEN" | python3 -c 'import sys,json;print(json.load(sys.stdin)[0]["id"])') ``` #### 3. Control , direct create is correctly blocked ```bash curl -s -o /dev/null -w '%{http_code}\n' \ -X POST "$OPENWEBUI/api/v1/calendars/events/create" \ -H "Authorization: Bearer $ATTACKER_TOKEN" -H 'Content-Type: application/json' \ -d "{\"calendar_id\":\"$VICTIM_CALENDAR_ID\",\"title\":\"x\",\"start_at\":1778400000000000000,\"end_at\":1778403600000000000}" # →

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
Severity from
GitHub (reviewed advisory)
Weakness
CWE-639
Also known as
CVE-2026-54006, PYSEC-2026-2722

More Open WebUI advisories

All Open WebUI

Critical advisories by email

Wednesdays: the week’s critical and high advisories in the AI and data stack, with the fixed versions. Only in weeks that have some.

Double opt-in. Unsubscribe any time.