GHSA-X975-RGX4-5FH4: Root-cause XSS Fix in appium-mcp MCP-UI
Summary
The patch addresses the XSS sink in the MCP-UI HTML generator by escaping all untrusted locator metadata before insertion into markup and by removing inline JavaScript event handlers that previously allowed attribute and script-context breakout. The added test exercises attacker-controlled tag, text, content description, resource ID, strategy, and selector values and verifies they are rendered as escaped text rather than executable HTML or injected handler code.
Analysis
Vulnerability
GHSA-X975-RGX4-5FH4 describes a cross-site scripting issue in the Appium MCP server's MCP-UI resource. The vulnerable UI builder interpolated untrusted layout metadata directly into HTML and into an inline onclick handler. Per the commit diff in the patch source, attacker-controlled values such as tagName, text, contentDesc, resourceId, locator strategy, and selector were inserted without HTML escaping.
This created multiple execution paths: HTML body injection via element content, attribute injection via interpolated values, and JavaScript-context injection via the inline handler onclick="testLocator('...')". Given the advisory's impact statement, successful script execution in the inspector WebView could be chained into host command execution through message-passing abuse.
<h3>${element.tagName}</h3>
${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>` : ''}
<button class="test-btn" onclick="testLocator('${strategy}', \`${selector.replace(/`/g, '\\`')}\`)">Test</button>The vulnerable pattern is source-grounded in the commit diff at GitHub commit e222bbbd6fe2b656a320efcd143563f08061a83d. Additional context is summarized by the advisory and report pages: GitHub Advisory, CVE Reports entry.
Patch
The patch makes two substantive changes. First, it introduces a centralized escapeHtml function and applies it to every untrusted value rendered into the UI. Second, it removes the inline onclick sink entirely and replaces it with delegated event handling that reads escaped values from data-* attributes.
<h3>${escapeHtml(element.tagName)}</h3>
${element.text ? `<p class="element-text"><strong>Text:</strong> ${escapeHtml(element.text)}</p>` : ''}
${element.contentDesc ? `<p class="element-text"><strong>Content Desc:</strong> ${escapeHtml(element.contentDesc)}</p>` : ''}
${element.resourceId ? `<p class="element-text"><strong>Resource ID:</strong> <code>${escapeHtml(element.resourceId)}</code></p>` : ''}
<span class="strategy">${escapeHtml(strategy)}</span>
<code class="selector">${escapeHtml(selector)}</code>
<button class="test-btn" data-strategy="${escapeHtml(strategy)}" data-selector="${escapeHtml(selector)}">Test</button>
document.addEventListener('click', (event) => {
const target = event.target;
const button = target instanceof Element ? target.closest('.test-btn') : null;
if (!button) {
return;
}
testLocator(button.dataset.strategy || '', button.dataset.selector || '');
});
function escapeHtml(value: unknown): string {
return String(value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}The accompanying test is strong evidence of intent and coverage. It feeds malicious values into all relevant fields and asserts that raw tags, event handlers, and injected HTML are absent from the generated output while escaped equivalents remain present. This directly validates both the content escaping and the removal of inline handler injection, as shown in the patch commit.
Review
Pros
- Addresses the actual sink classes visible in the diff: HTML text-node injection and inline event-handler injection.
- Uses a single escaping primitive consistently across all user-controlled fields rendered by the UI generator.
- Eliminates inline JavaScript construction, which is materially safer than trying to quote-escape attacker input inside an
onclickattribute. - Switches to event delegation with
data-strategyanddata-selector, reducing exposure to script-context parsing bugs. - Adds a regression test with realistic payloads including tag injection, attribute injection, and
postMessage-oriented payload content.
Cons
- The reviewable patch scope is limited to
src/ui/mcp-ui-utils.ts; it does not prove that all other UI rendering paths in the project follow the same escaping discipline. escapeHtmlis context-specific. It is correct for HTML text and double-quoted attribute contexts used here, but it would not be sufficient for URL, CSS, or script contexts if future code reuses it incorrectly.- The delegated click handler trusts
datasetvalues as plain strings. That is acceptable for this sink, but downstream consumers such astestLocatorstill need their own validation for protocol or command boundaries.
Verdict
Root-cause.
The patch fixes the root cause exposed by the cited sources: untrusted locator metadata was being inserted into executable and parseable HTML/JS contexts without output encoding. By escaping all rendered fields and removing the inline onclick sink, the patch closes the demonstrated XSS vectors rather than merely filtering a subset of payloads. Based on the available diff and tests in the commit, this is a technically sound remediation for the vulnerable MCP-UI generator described in the advisory.
Recommended Labs
Try this vulnerability pattern yourself with hands-on labs.
- Electron XSS.js
Best match for this advisory’s client-side code execution pattern. The GHSA describes unescaped UI/locator metadata executing in a client inspector WebView, which is very close to frontend/Electron-style XSS where unsafe rendering in a privileged desktop-web context can have host impact. Good defensive lab for learning output encoding and safer rendering boundaries.
- React XSS.js
Strong fit for practicing DOM/client-side XSS defenses in modern JavaScript UI code. Since the vulnerable Appium MCP component renders unescaped locator/layout data into an inspector UI, this lab is useful for learning how unsafe component rendering leads to script execution and how to fix it with proper escaping and safer data handling.
- React XSS3.js
A good step-up lab after the easier React XSS exercise. The advisory suggests more than simple reflection: untrusted metadata crosses a trust boundary into a privileged UI and is then exploitable via postMessage. This medium-difficulty lab is useful for practicing defense-in-depth fixes for richer client-side XSS scenarios.