Skip to content

fix: add path traversal validation to install_app tool (CWE-22)#300

Open
sebastiondev wants to merge 1 commit intomobile-next:mainfrom
sebastiondev:fix/cwe22-server-mobile-b85d
Open

fix: add path traversal validation to install_app tool (CWE-22)#300
sebastiondev wants to merge 1 commit intomobile-next:mainfrom
sebastiondev:fix/cwe22-server-mobile-b85d

Conversation

@sebastiondev
Copy link
Copy Markdown

Vulnerability Summary

CWE-22: Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal")
Severity: Medium
Affected tool: mobile_install_app

Data Flow

  1. MCP client (AI agent / LLM) sends a mobile_install_app tool call with an attacker-controlled path parameter (type: z.string(), no validation).
  2. path flows directly into robot.installApp(path).
  3. This reaches execFileSync(adbPath, ["install", "-r", path]) (Android) or execFileSync("xcrun", ["simctl", "install", uuid, path]) (iOS simulator) or ios("install", "--path", path) (iOS physical device).
  4. An arbitrary .apk or .ipa file from any location on the host filesystem is installed onto the connected mobile device.

Exploit Scenario

A prompt-injected AI agent (or malicious MCP client) can sideload arbitrary app packages from anywhere on the host filesystem:

{
  "tool": "mobile_install_app",
  "arguments": {
    "device": "emulator-5554",
    "path": "/home/victim/.cache/downloads/malware.apk"
  }
}

Or with traversal:

{
  "tool": "mobile_install_app",
  "arguments": {
    "device": "emulator-5554",
    "path": "../../tmp/evil-sideload.apk"
  }
}

Preconditions

  1. Attacker must be able to influence the LLM's tool calls (prompt injection or malicious MCP client)
  2. A mobile device must be connected and accessible
  3. The malicious app file must already exist on the host filesystem
  4. The MCP server must be running

Fix Description

This PR adds two validations to install_app, bringing it to parity with save_screenshot and start_screen_recording (which were fixed in v0.0.49, PR #296):

  1. validateFileExtension(path, ALLOWED_APP_EXTENSIONS, "install_app") — restricts to .apk, .ipa, .zip, .app
  2. validateInputPath(path) — new function that reuses the existing validatePathAgainstAllowedRoots() logic to confine paths to cwd and os.tmpdir()

Rationale

  • The existing validateOutputPath() was refactored to extract a shared validatePathAgainstAllowedRoots(filePath, label) helper, and validateInputPath() was added as a thin wrapper. This avoids code duplication and keeps the security boundary consistent for both input and output paths.
  • The ALLOWED_APP_EXTENSIONS list (.apk, .ipa, .zip, .app) covers all formats accepted by adb install, simctl install, and go-ios install.
  • The open_url scheme restriction (also on this branch from upstream commit a0aa689) is a separate upstream fix included in this branch's base.

Changes (4 files, +105 / −2 lines)

File Change
src/server.ts Add validateFileExtension + validateInputPath calls in install_app handler
src/utils.ts Extract validatePathAgainstAllowedRoots helper; export new validateInputPath
test/utils.ts 12 new test cases covering path traversal rejection and extension validation
CHANGELOG.md Document the v0.0.50 entry (already present from upstream)

Test Results

12 new tests added in test/utils.ts:

validateInputPath tests (6):

  • ✅ Allows paths under cwd
  • ✅ Rejects paths outside allowed roots (e.g., /etc/passwd)
  • ✅ Rejects ../ traversal attempts from cwd
  • ✅ Rejects absolute paths to /usr
  • ✅ Rejects paths under /home or /Users outside cwd
  • ✅ Rejects paths to root filesystem

validateFileExtension tests (6):

  • ✅ Accepts .apk files
  • ✅ Accepts .ipa files
  • ✅ Accepts .zip files
  • ✅ Accepts .app paths
  • ✅ Rejects other extensions (e.g., .sh)
  • ✅ Rejects files with no extension

Disprove Analysis

We attempted to invalidate this finding through 9 checks:

Check Result
Auth No authentication on MCP server. SSE mode has no auth middleware; stdio mode trusts the calling process.
Network SSE mode binds 0.0.0.0 with no CORS/host restrictions. Stdio is process-local but the AI agent IS the threat actor in prompt injection.
Deployment Developer tool, no containerization. Typically run via npx locally.
Caller trace path parameter flows from MCP client → z.string()robot.installApp(path)execFileSync(...) with zero validation.
Existing validation Zero validation on path in install_app. Adjacent tools (save_screenshot, start_screen_recording) DO validate since v0.0.49.
Prior reports No existing issues, but maintainers already fixed the same CWE-22 pattern in PR #296 (v0.0.49).
Security policy SECURITY.md exists; all versions supported for security updates.
Recent commits f5e3229 fixed path traversal in screenshot/recording. a0aa689 restricted URL schemes. Both demonstrate maintainer receptiveness.
Fix adequacy Fix is not cosmetic — it closes a real gap where install_app was missed during the v0.0.49 path validation effort.

Mitigating Factors (documented for completeness)

  1. execFileSync with array args prevents shell injection — the issue is strictly path traversal, not RCE
  2. adb install / simctl install reject non-valid packages — limits impact to actual .apk/.ipa files
  3. Stdio transport is process-local (but the AI agent is the threat vector in prompt injection)

Verdict

CONFIRMED VALID — High confidence. The install_app tool was simply missed when path validation was added in v0.0.49. This PR brings it to parity.


Prior Art

Thank you for maintaining this project and for the existing security work in v0.0.49. This PR simply extends that same protection to the install_app tool that was missed.

…WE-22)

Add validateInputPath() and validateFileExtension() checks to the
mobile_install_app tool handler so that the user-supplied path parameter
is constrained to the current working directory and temp directory,
matching the existing validation applied to save_screenshot and
start_screen_recording.

Without this check, a prompt-injected AI agent could call
mobile_install_app with an arbitrary filesystem path, causing
installation of a malicious package from outside the expected
directories.

Changes:
- src/utils.ts: refactor validateOutputPath into a shared
  validatePathAgainstAllowedRoots helper; export new validateInputPath
- src/server.ts: add ALLOWED_APP_EXTENSIONS constant; call
  validateFileExtension and validateInputPath before robot.installApp()
- test/utils.ts: new test suite covering path traversal rejection and
  extension validation for install_app
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 28, 2026

Walkthrough

This pull request introduces input validation for the mobile app installation feature. The mobile_install_app handler now validates both file extensions and file paths before installation. A new validateInputPath() function was exported from utils and reuses centralized path validation logic. The existing validateOutputPath() was refactored to delegate to an internal helper function. Corresponding test coverage was added to verify that validateInputPath() accepts paths within the current working directory while rejecting paths outside it, and that file extension validation only permits .apk, .ipa, .zip, and .app files.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding path traversal validation to the install_app tool to address CWE-22, which aligns with the core objective of the changeset.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the vulnerability, fix implementation, test coverage, and security analysis.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
test/utils.ts (1)

10-52: Test coverage is Unix-centric; consider cross-platform and symlink scenarios.

The test suite effectively covers common path traversal attack vectors on Unix systems. However:

  1. Windows coverage: Tests use hardcoded Unix paths (/etc, /usr, /Users). Consider adding platform-conditional tests for Windows paths or skipping Unix-specific tests when process.platform === "win32".

  2. Symlink resolution: The shared helper validatePathAgainstAllowedRoots includes symlink resolution logic via resolveWithSymlinks, but no tests verify that a symlink pointing outside allowed roots is correctly rejected.

  3. Linux home directory: Line 45 tests /Users (macOS) but not /home (Linux).

♻️ Example symlink test case
it("should reject symlinks pointing outside allowed roots", function() {
	if (process.platform === "win32") {
		this.skip();
		return;
	}
	const linkPath = path.join(process.cwd(), "malicious-link.apk");
	try {
		fs.symlinkSync("/etc/passwd", linkPath);
		assert.throws(() => validateInputPath(linkPath), ActionableError);
	} finally {
		try { fs.unlinkSync(linkPath); } catch {}
	}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/utils.ts` around lines 10 - 52, Add cross-platform and symlink tests for
validateInputPath: skip or adapt Unix-specific tests when process.platform ===
"win32" (so tests asserting /etc, /usr, /Users only run on non-Windows), add an
additional Linux home test using "/home/..." alongside the existing "/Users/..."
case, and add a symlink test that creates a symlink in the cwd pointing to an
outside path (e.g., /etc/passwd) and asserts validateInputPath rejects it;
reference validateInputPath, validatePathAgainstAllowedRoots and its
resolveWithSymlinks behavior to ensure the symlink case exercises resolution
logic and gets cleaned up in finally blocks.
src/utils.ts (1)

69-88: Unused label parameter in validatePathAgainstAllowedRoots.

The label parameter is declared but never referenced in the function body. The error message on line 85 is generic and doesn't distinguish between "input" vs "output" validation contexts.

Consider either removing the parameter or incorporating it into the error message for clearer debugging:

♻️ Proposed fix to use the label parameter
 function validatePathAgainstAllowedRoots(filePath: string, label: string): void {
 	const resolved = resolveWithSymlinks(filePath);
 	const allowedRoots = getAllowedRoots();
 	const isWindows = process.platform === "win32";

 	const isAllowed = allowedRoots.some(root => {
 		if (isWindows) {
 			return isPathUnderRoot(resolved.toLowerCase(), root.toLowerCase());
 		}

 		return isPathUnderRoot(resolved, root);
 	});

 	if (!isAllowed) {
 		const dir = path.dirname(resolved);
 		throw new ActionableError(
-			`"${dir}" is not in the list of allowed directories. Allowed directories include the current directory and the temp directory on this host.`
+			`The ${label} path "${dir}" is not in the list of allowed directories. Allowed directories include the current directory and the temp directory on this host.`
 		);
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils.ts` around lines 69 - 88, The validatePathAgainstAllowedRoots
function currently accepts an unused label parameter; update the function to
incorporate label into the thrown ActionableError message (or remove the
parameter if callers don't pass context). Specifically, keep the existing
resolution logic (resolveWithSymlinks, getAllowedRoots, isPathUnderRoot) but
change the error to include the label and the offending directory (for example:
`ActionableError(\`${label} "${dir}" is not in the list of allowed
directories...\`)`) so callers know whether this was an input or output
validation; if you choose to remove the parameter, also remove it from all call
sites invoking validatePathAgainstAllowedRoots.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/utils.ts`:
- Around line 69-88: The validatePathAgainstAllowedRoots function currently
accepts an unused label parameter; update the function to incorporate label into
the thrown ActionableError message (or remove the parameter if callers don't
pass context). Specifically, keep the existing resolution logic
(resolveWithSymlinks, getAllowedRoots, isPathUnderRoot) but change the error to
include the label and the offending directory (for example:
`ActionableError(\`${label} "${dir}" is not in the list of allowed
directories...\`)`) so callers know whether this was an input or output
validation; if you choose to remove the parameter, also remove it from all call
sites invoking validatePathAgainstAllowedRoots.

In `@test/utils.ts`:
- Around line 10-52: Add cross-platform and symlink tests for validateInputPath:
skip or adapt Unix-specific tests when process.platform === "win32" (so tests
asserting /etc, /usr, /Users only run on non-Windows), add an additional Linux
home test using "/home/..." alongside the existing "/Users/..." case, and add a
symlink test that creates a symlink in the cwd pointing to an outside path
(e.g., /etc/passwd) and asserts validateInputPath rejects it; reference
validateInputPath, validatePathAgainstAllowedRoots and its resolveWithSymlinks
behavior to ensure the symlink case exercises resolution logic and gets cleaned
up in finally blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 405164c2-adec-47ca-865b-cf9b9c27ce51

📥 Commits

Reviewing files that changed from the base of the PR and between 5415d17 and 46a4075.

📒 Files selected for processing (3)
  • src/server.ts
  • src/utils.ts
  • test/utils.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant