From ef2db345ca7d1dfcc7e2b290936da1ab6a531e1d Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Wed, 1 Apr 2026 14:26:49 +0100 Subject: [PATCH] fix: prevent shell injection by replacing exec() with execFile() Using exec() with string interpolation allows shell metacharacter injection when URLs or API-sourced values (e.g. testCaseKey) contain special characters. Switching to execFile() with argument arrays bypasses the shell entirely, eliminating the injection surface. --- src/commands/open.ts | 14 ++++++++----- src/play/tui/App.tsx | 49 +++++++++++++++++++++++--------------------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/commands/open.ts b/src/commands/open.ts index 5bb6f29..d01f1ff 100644 --- a/src/commands/open.ts +++ b/src/commands/open.ts @@ -1,4 +1,4 @@ -import { exec } from "node:child_process"; +import { execFile } from "node:child_process"; import type { Command } from "commander"; import { getProfile, loadConfig } from "../config/manager"; import type { GlobalOptions } from "../config/types"; @@ -30,18 +30,22 @@ function buildZephyrUrl( function openInBrowser(url: string): void { const platform = process.platform; let command: string; + let args: string[]; if (platform === "darwin") { - command = `open "${url}"`; + command = "open"; + args = [url]; } else if (platform === "linux") { - command = `xdg-open "${url}"`; + command = "xdg-open"; + args = [url]; } else if (platform === "win32") { - command = `start "" "${url}"`; + command = "cmd"; + args = ["/c", "start", "", url]; } else { throw new Error(`Unsupported platform: ${platform}. Please open manually:\n${url}`); } - exec(command, (error) => { + execFile(command, args, (error) => { if (error) { logger.error(`Failed to open browser: ${error.message}`); console.log(`URL: ${url}`); diff --git a/src/play/tui/App.tsx b/src/play/tui/App.tsx index d9e2908..9263d0d 100644 --- a/src/play/tui/App.tsx +++ b/src/play/tui/App.tsx @@ -1,4 +1,4 @@ -import { exec } from "node:child_process"; +import { execFile } from "node:child_process"; import { Box, render, useApp, useInput, useStdout } from "ink"; import { useCallback, useMemo, useRef, useState } from "react"; import type { ZephyrV2Client } from "zephyr-api-client"; @@ -295,21 +295,24 @@ function App({ const url = `${base}/projects/${projectKey}?selectedItem=com.atlassian.plugins.atlassian-connect-plugin:com.kanoah.test-manager__main-project-page#!/v2/testPlayer/${cycleKey}`; // Copy test case key to clipboard for pasting into the search box - const clipCmd = - process.platform === "darwin" - ? "pbcopy" - : process.platform === "win32" - ? "clip" - : "xclip -selection clipboard"; - exec(`printf '%s' "${testCaseKey}" | ${clipCmd}`); - - const openCmd = - process.platform === "darwin" - ? `open "${url}"` - : process.platform === "win32" - ? `start "" "${url}"` - : `xdg-open "${url}"`; - exec(openCmd); + if (process.platform === "darwin") { + execFile("pbcopy", [], (err) => err && actions.setError(`Clipboard failed: ${err.message}`)) + .stdin?.end(testCaseKey); + } else if (process.platform === "win32") { + execFile("clip", [], (err) => err && actions.setError(`Clipboard failed: ${err.message}`)) + .stdin?.end(testCaseKey); + } else { + execFile("xclip", ["-selection", "clipboard"], (err) => err && actions.setError(`Clipboard failed: ${err.message}`)) + .stdin?.end(testCaseKey); + } + + if (process.platform === "darwin") { + execFile("open", [url]); + } else if (process.platform === "win32") { + execFile("cmd", ["/c", "start", "", url]); + } else { + execFile("xdg-open", [url]); + } setSyncMessage(`Copied "${testCaseKey}" - paste in Search box`); if (syncMessageTimerRef.current) clearTimeout(syncMessageTimerRef.current); @@ -326,13 +329,13 @@ function App({ const base = jiraBaseUrl.replace(/\/+$/, ""); const testCaseKey = selectedTestCase.testCase.key; const url = `${base}/projects/${projectKey}?selectedItem=com.atlassian.plugins.atlassian-connect-plugin:com.kanoah.test-manager__main-project-page#!/v2/testCase/${testCaseKey}`; - const openCmd = - process.platform === "darwin" - ? `open "${url}"` - : process.platform === "win32" - ? `start "" "${url}"` - : `xdg-open "${url}"`; - exec(openCmd); + if (process.platform === "darwin") { + execFile("open", [url]); + } else if (process.platform === "win32") { + execFile("cmd", ["/c", "start", "", url]); + } else { + execFile("xdg-open", [url]); + } } return; }