GHSA-f3g7-59qc-pqg6: Open WebUI IDOR: Calendar event re-parenting allows writing events into another user's calendar
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`.DetailsSink — missing destination check`backend/open_webui/routers/calendar.py:283-297``backend/open_webui/models/calendar.py:658-693` (`update_event_by_id`)Reference — `create_event` does check the destination`backend/open_webui/routers/calendar.py:255`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 unconditionallyPoCVerified 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. EnvironmentCreate 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:3. Control — direct create is correctly blocked4. Exploit — create-then-reparent5. Verification from victim's sessionObserved output (truncated):The injected event now lives in the victim's default calendar. A subsequent `GET /events/{id}` as the **attacker** returns **403** — confirming the move succeeded and the attacker has no legitimate access to the destination.Impact**Read-only → write escalation** on shared calendars: a user granted `read` via `AccessGrants` can effectively write.**Phishing / social engineering**: events appear inside the victim's own private calendar (not as an external invite). The hover tooltip (`CalendarEventChip.svelte:12 → common/Tooltip.svelte`) renders `title`/`location` as DOMPurify-sanitised HTML with `allowHTML=true`, so an attacker can embed formatted links and `<img>` beacons (read-receipt when the victim hovers). DOMPurify prevents script execution, so this is HTML injection, not XSS.**Calendar spam / DoS**: unlimited one-shot injections (attacker loses access to each event after the move, but can repeat with new events).
CVSS v3.1: 4.3