From 7d3e1c06888e346b26a9c9c76e0b64d2cb61d621 Mon Sep 17 00:00:00 2001 From: Lucy Gramley Date: Tue, 19 May 2026 15:32:54 -0700 Subject: [PATCH] Harden Network Inspector server binding and adb command execution - Bind RSocket TCP server to 127.0.0.1 instead of all interfaces - Strengthen filepath validation with strict allowlist and path traversal check - Refactor adb command execution to use execFile with argument arrays instead of string concatenation via exec - Update all executeQuery callers to pass argument arrays Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/extension/android/adb.ts | 32 +++++++++++-------- .../android/androidContainerUtility.ts | 11 ++++--- src/extension/android/androidPlatform.ts | 2 +- .../networkInspectorServer.ts | 1 + 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/extension/android/adb.ts b/src/extension/android/adb.ts index 9a3a24fec..16e88544b 100644 --- a/src/extension/android/adb.ts +++ b/src/extension/android/adb.ts @@ -164,12 +164,12 @@ export class AdbHelper { } public async apiVersion(deviceId: string): Promise { - const output = await this.executeQuery(deviceId, "shell getprop ro.build.version.sdk"); + const output = await this.executeQuery(deviceId, ["shell", "getprop", "ro.build.version.sdk"]); return parseInt(output, 10); } public reverseAdb(deviceId: string, port: number): Promise { - return this.execute(deviceId, `reverse tcp:${port} tcp:${port}`); + return this.execute(deviceId, ["reverse", `tcp:${port}`, `tcp:${port}`]); } public showDevMenu(deviceId?: string): Promise { @@ -237,7 +237,7 @@ export class AdbHelper { ); const isExist = fs.existsSync(localPropertiesSdkPath); if (isExist) { - return `"${localPropertiesSdkPath}"`; + return localPropertiesSdkPath; } if (logger) { logger.warning( @@ -253,15 +253,24 @@ export class AdbHelper { } public executeShellCommand(deviceId: string, command: string): Promise { - return this.executeQuery(deviceId, `shell "${command}"`); + return this.childProcess.execFileToString(this.adbExecutable, [ + "-s", + deviceId, + "shell", + command, + ]); } public installApplicationToEmulator(appPath: string): Promise { - return this.childProcess.execToString(`adb install ${appPath}`); + return this.childProcess.execFileToString("adb", ["install", appPath]); } - public executeQuery(deviceId: string, command: string): Promise { - return this.childProcess.execToString(this.generateCommandForTarget(deviceId, command)); + public executeQuery(deviceId: string, args: string[]): Promise { + return this.childProcess.execFileToString(this.adbExecutable, [ + "-s", + deviceId, + ...args, + ]); } private parseConnectedTargets(input: string): IDebuggableMobileTarget[] { @@ -283,12 +292,9 @@ export class AdbHelper { return !!id.match(AdbHelper.AndroidSDKEmulatorPattern); } - private execute(deviceId: string, command: string): Promise { - return this.commandExecutor.execute(this.generateCommandForTarget(deviceId, command)); - } - - private generateCommandForTarget(deviceId: string, adbCommand: string): string { - return `${this.adbExecutable} -s "${deviceId}" ${adbCommand}`; + private execute(deviceId: string, args: string[]): Promise { + const command = `${this.adbExecutable} -s "${deviceId}" ${args.join(" ")}`; + return this.commandExecutor.execute(command); } private getSdkLocationFromLocalPropertiesFile( diff --git a/src/extension/android/androidContainerUtility.ts b/src/extension/android/androidContainerUtility.ts index 6f3aaed14..f293025c3 100644 --- a/src/extension/android/androidContainerUtility.ts +++ b/src/extension/android/androidContainerUtility.ts @@ -104,7 +104,7 @@ async function _pushFile( try { const pushRes = await adbHelper.executeQuery( deviceId, - `push ${sourceFilepath} ${tmpFilePath}`, + ["push", sourceFilepath, tmpFilePath], ); logger?.debug(pushRes); const command = `cp "${tmpFilePath}" "${destFilepath}" && chmod 644 "${destFilepath}"`; @@ -135,10 +135,13 @@ function validateAppName(app: string): Promise { } function validateFilePath(filePath: string): Promise { - if (!filePath.match(/'/)) { - return Promise.resolve(filePath); + if (!/^[A-Za-z0-9._\/-]+$/.test(filePath)) { + return Promise.reject(new Error(`Disallowed filepath characters: ${filePath}`)); } - return Promise.reject(new Error(`Disallowed escaping filepath: ${filePath}`)); + if (filePath.includes("..")) { + return Promise.reject(new Error(`Path traversal not allowed: ${filePath}`)); + } + return Promise.resolve(filePath); } function validateFileContent(content: string): Promise { diff --git a/src/extension/android/androidPlatform.ts b/src/extension/android/androidPlatform.ts index 7aac81529..83c404dc5 100644 --- a/src/extension/android/androidPlatform.ts +++ b/src/extension/android/androidPlatform.ts @@ -226,7 +226,7 @@ export class AndroidPlatform extends GeneralMobilePlatform { // For physical devices, get model name const modelResult = await this.adbHelper.executeQuery( targetId, - "shell getprop ro.product.model", + ["shell", "getprop", "ro.product.model"], ); const model = modelResult.trim(); if (model) { diff --git a/src/extension/networkInspector/networkInspectorServer.ts b/src/extension/networkInspector/networkInspectorServer.ts index 6d63ffce6..933c4647e 100644 --- a/src/extension/networkInspector/networkInspectorServer.ts +++ b/src/extension/networkInspector/networkInspectorServer.ts @@ -156,6 +156,7 @@ export class NetworkInspectorServer { : this.untrustedRequestHandler, transport: new RSocketTCPServer({ port: port, + host: "127.0.0.1", serverFactory: serverFactory, }), });