From 08a67cb8d50da8c8381678f7723c784f3e08f29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Wed, 5 Nov 2025 17:13:41 +0100 Subject: [PATCH] fix(utils): quote shell arguments to prevent malicious injection --- package-lock.json | 21 +++++++++++++++++++ package.json | 2 ++ .../plugin-coverage/src/lib/runner/index.ts | 3 +-- packages/plugin-eslint/src/lib/runner/lint.ts | 15 +++---------- .../src/lib/runner/index.ts | 3 +-- packages/utils/src/lib/execute-process.ts | 5 ++++- testing/test-nx-utils/src/lib/utils/nx.ts | 2 +- 7 files changed, 33 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index b8d79a01d..52dee866e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "parse-lcov": "^1.0.4", "rimraf": "^6.0.1", "semver": "^7.6.3", + "shell-quote": "^1.8.3", "simple-git": "^3.26.0", "ts-morph": "^24.0.0", "tslib": "^2.6.2", @@ -66,6 +67,7 @@ "@types/node": "^22.13.4", "@types/react": "18.3.1", "@types/react-dom": "18.3.0", + "@types/shell-quote": "^1.7.5", "@vitejs/plugin-react": "^5.0.0", "@vitest/coverage-v8": "1.3.1", "@vitest/eslint-plugin": "^1.1.38", @@ -9089,6 +9091,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/shell-quote": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.5.tgz", + "integrity": "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -27500,6 +27509,18 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", diff --git a/package.json b/package.json index a5fec600c..80ea748fa 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "parse-lcov": "^1.0.4", "rimraf": "^6.0.1", "semver": "^7.6.3", + "shell-quote": "^1.8.3", "simple-git": "^3.26.0", "ts-morph": "^24.0.0", "tslib": "^2.6.2", @@ -76,6 +77,7 @@ "@types/node": "^22.13.4", "@types/react": "18.3.1", "@types/react-dom": "18.3.0", + "@types/shell-quote": "^1.7.5", "@vitejs/plugin-react": "^5.0.0", "@vitest/coverage-v8": "1.3.1", "@vitest/eslint-plugin": "^1.1.38", diff --git a/packages/plugin-coverage/src/lib/runner/index.ts b/packages/plugin-coverage/src/lib/runner/index.ts index f22ed5476..c5818ab79 100644 --- a/packages/plugin-coverage/src/lib/runner/index.ts +++ b/packages/plugin-coverage/src/lib/runner/index.ts @@ -7,7 +7,6 @@ import { createRunnerFiles, ensureDirectoryExists, executeProcess, - filePathToCliArg, objectToCliArgs, readJsonFile, ui, @@ -66,7 +65,7 @@ export async function createRunnerConfig( return { command: 'node', args: [ - filePathToCliArg(scriptPath), + scriptPath, ...objectToCliArgs({ runnerConfigPath, runnerOutputPath }), ], configFile: runnerConfigPath, diff --git a/packages/plugin-eslint/src/lib/runner/lint.ts b/packages/plugin-eslint/src/lib/runner/lint.ts index b014738c1..97f82acc3 100644 --- a/packages/plugin-eslint/src/lib/runner/lint.ts +++ b/packages/plugin-eslint/src/lib/runner/lint.ts @@ -1,11 +1,5 @@ import type { ESLint, Linter } from 'eslint'; -import { platform } from 'node:os'; -import { - distinct, - executeProcess, - filePathToCliArg, - toArray, -} from '@code-pushup/utils'; +import { distinct, executeProcess, toArray } from '@code-pushup/utils'; import type { ESLintTarget } from '../config.js'; import { setupESLint } from '../setup.js'; import type { LinterOutput, RuleOptionsPerFile } from './types.js'; @@ -29,14 +23,11 @@ async function executeLint({ command: 'npx', args: [ 'eslint', - ...(eslintrc ? [`--config=${filePathToCliArg(eslintrc)}`] : []), + ...(eslintrc ? [`--config=${eslintrc}`] : []), ...(typeof eslintrc === 'object' ? ['--no-eslintrc'] : []), '--no-error-on-unmatched-pattern', '--format=json', - ...toArray(patterns).map(pattern => - // globs need to be escaped on Unix - platform() === 'win32' ? pattern : `'${pattern}'`, - ), + ...toArray(patterns), ], ignoreExitCode: true, cwd: process.cwd(), diff --git a/packages/plugin-js-packages/src/lib/runner/index.ts b/packages/plugin-js-packages/src/lib/runner/index.ts index 98fc1047f..ff47788f9 100644 --- a/packages/plugin-js-packages/src/lib/runner/index.ts +++ b/packages/plugin-js-packages/src/lib/runner/index.ts @@ -5,7 +5,6 @@ import { createRunnerFiles, ensureDirectoryExists, executeProcess, - filePathToCliArg, isPromiseFulfilledResult, isPromiseRejectedResult, objectFromEntries, @@ -39,7 +38,7 @@ export async function createRunnerConfig( return { command: 'node', args: [ - filePathToCliArg(scriptPath), + scriptPath, ...objectToCliArgs({ runnerConfigPath, runnerOutputPath }), ], configFile: runnerConfigPath, diff --git a/packages/utils/src/lib/execute-process.ts b/packages/utils/src/lib/execute-process.ts index d1fa98a3f..f1624aa35 100644 --- a/packages/utils/src/lib/execute-process.ts +++ b/packages/utils/src/lib/execute-process.ts @@ -6,6 +6,7 @@ import { spawn, } from 'node:child_process'; import type { Readable, Writable } from 'node:stream'; +import { quote } from 'shell-quote'; import { isVerbose } from './env.js'; import { formatCommandLog } from './format-command-log.js'; import { ui } from './logging.js'; @@ -157,9 +158,11 @@ export function executeProcess(cfg: ProcessConfig): Promise { ); } + const bin = [command, quote(args ?? [])].join(' '); + return new Promise((resolve, reject) => { // shell:true tells Windows to use shell command for spawning a child process - const spawnedProcess = spawn(command, args ?? [], { + const spawnedProcess = spawn(bin, { shell: true, windowsHide: true, ...options, diff --git a/testing/test-nx-utils/src/lib/utils/nx.ts b/testing/test-nx-utils/src/lib/utils/nx.ts index 050bdc541..fecf433fa 100644 --- a/testing/test-nx-utils/src/lib/utils/nx.ts +++ b/testing/test-nx-utils/src/lib/utils/nx.ts @@ -84,7 +84,7 @@ export async function nxShowProjectJson( ) { const { code, stderr, stdout } = await executeProcess({ command: 'npx', - args: ['nx', 'show', `project --json ${project}`], + args: ['nx', 'show', 'project', '--json', project], cwd, });