Conversation
99949d0 to
495a83d
Compare
| try { | ||
| // tslint:disable-next-line:object-literal-sort-keys | ||
| Util.spawnSync('npm', ['whoami', `--registry=${fullPackageRegistry}`], { stdio: 'pipe', encoding: 'utf8' }); | ||
| Util.execSync(`npm whoami --registry=${fullPackageRegistry}`, { stdio: "pipe", encoding: "utf8" }); |
Check warning
Code scanning / CodeQL
Unsafe shell command constructed from library input Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 13 hours ago
To fix the problem, avoid constructing shell command strings that embed fullPackageRegistry and instead supply the registry value as an argument to a child process API that does not invoke a shell (for example, child_process.spawnSync with shell: false). This prevents shell interpretation of metacharacters and sidesteps reliance on custom sanitization. The behavior (running npm whoami, npm login, and npm config set) should be preserved while changing only how these commands are invoked.
Concretely, we will:
- Add a small helper
execSyncCmdinUtilthat wrapsspawnSyncto execute a command with an argv array and similar error handling to the existingexecSync. - Update
PackageManager.ensureRegistryUserso that:npm whoamiis executed asUtil.execSyncCmd("npm", ["whoami",--registry=${fullPackageRegistry}], { ... }).npm loginis executed similarly with its arguments array.npm config setis executed asUtil.execSyncCmd("npm", ["config", "set", "@infragistics:registry", fullPackageRegistry]);.
- Keep
Util.sanitizeShellArgin place (it still helps validate/normalize the registry), but no longer depend on it for shell safety.
All changes will be contained within the shown snippets in packages/core/util/Util.ts (add the helper method) and packages/core/packages/PackageManager.ts (switch calls from execSync with a string to the new helper with argv arrays).
| @@ -227,7 +227,7 @@ | ||
| const fullPackageRegistry = Util.sanitizeShellArg(config.igPackageRegistry); | ||
| try { | ||
| // tslint:disable-next-line:object-literal-sort-keys | ||
| Util.execSync(`npm whoami --registry=${fullPackageRegistry}`, { stdio: "pipe", encoding: "utf8" }); | ||
| Util.execSyncCmd("npm", ["whoami", `--registry=${fullPackageRegistry}`], { stdio: "pipe", encoding: "utf8" }); | ||
| } catch (error) { | ||
| // try registering the user: | ||
| Util.log( | ||
| @@ -250,12 +250,13 @@ | ||
| } | ||
|
|
||
| try { | ||
| Util.execSync( | ||
| `npm login --registry=${fullPackageRegistry} --scope=@infragistics --auth-type=legacy`, | ||
| Util.execSyncCmd( | ||
| "npm", | ||
| ["login", `--registry=${fullPackageRegistry}`, "--scope=@infragistics", "--auth-type=legacy"], | ||
| { stdio: "inherit" } | ||
| ); | ||
| //make sure scope is configured: | ||
| Util.execSync(`npm config set @infragistics:registry ${fullPackageRegistry}`); | ||
| Util.execSyncCmd("npm", ["config", "set", "@infragistics:registry", fullPackageRegistry]); | ||
| return true; | ||
| } catch (error) { | ||
| Util.log(message, "red"); |
| @@ -363,6 +363,43 @@ | ||
| } | ||
|
|
||
| /** | ||
| * Execute synchronous command without using a shell, accepting arguments as an array. | ||
| * Mirrors the error-handling semantics of execSync above. | ||
| * @param command Command to be executed (binary name) | ||
| * @param args Arguments to pass to the command | ||
| * @param options Spawn options | ||
| */ | ||
| public static execSyncCmd(command: string, args: string[], options?: SpawnSyncOptions) { | ||
| const spawnOptions: SpawnSyncOptions = Object.assign({}, options, { shell: false }); | ||
| const result = spawnSync(command, args, spawnOptions); | ||
|
|
||
| if (result.error) { | ||
| throw result.error; | ||
| } | ||
|
|
||
| // Non-zero exit code or terminated by signal | ||
| if (result.status !== 0) { | ||
| const error: any = new Error(`Command failed: ${command} ${args.join(" ")}`); | ||
| error.status = result.status; | ||
| error.signal = result.signal; | ||
| error.stdout = result.stdout; | ||
| error.stderr = result.stderr; | ||
|
|
||
| if (error.stderr && error.stderr.toString().endsWith() === "^C") { | ||
| return process.exit(); | ||
| } | ||
|
|
||
| if (error.status === 3221225786 || error.status > 128) { | ||
| return process.exit(); | ||
| } | ||
|
|
||
| throw error; | ||
| } | ||
|
|
||
| return result.stdout; | ||
| } | ||
|
|
||
| /** | ||
| * Execute synchronous command with options using spawnSync | ||
| * @param command Command to be executed | ||
| * NOTE: `spawn` without `shell` (unsafe) is **not** equivalent to `exec` & requires direct path to run the correct process on win, |
| //make sure scope is configured: | ||
| try { | ||
| Util.spawnSync('npm', ['config', 'set', `@infragistics:registry`, fullPackageRegistry]); | ||
| Util.execSync(`npm config set @infragistics:registry ${fullPackageRegistry}`); |
Check warning
Code scanning / CodeQL
Unsafe shell command constructed from library input Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 12 hours ago
In general terms, the fix is to stop interpolating fullPackageRegistry into a shell command string and instead pass it as a separate argument to a non‑shell API. That means replacing execSync with execFileSync (which takes a program and an array of arguments) or, for the wrapper, adding a variant that accepts command and args separately. This avoids any shell parsing of the argument, so even if fullPackageRegistry contains problematic characters, they will be treated as literal argument content, not control characters.
The minimal, behavior‑preserving change here is to (1) add a new helper in Util that uses execFileSync with an explicit command and argument array, and (2) update PackageManager.ensureRegistryUser to call this new helper instead of building a string command. Specifically:
- In
packages/core/util/Util.ts, add a static method, e.g.execFileSync(command: string, args: string[], options?: ExecSyncOptions), that wraps Node’schild_process.execFileSync. This keeps the same error‑handling semantics as the currentexecSyncwrapper. We must also importexecFileSyncfromchild_process. - In
packages/core/packages/PackageManager.ts, change the threeUtil.execSync(...)calls insideensureRegistryUserto useUtil.execFileSyncwith explicit argument arrays:npm whoami --registry=<url>→Util.execFileSync("npm", ["whoami",--registry=${fullPackageRegistry}], { ... })npm login --registry=<url> --scope=@infragistics --auth-type=legacy→Util.execFileSync("npm", ["login",--registry=${fullPackageRegistry}, "--scope=@infragistics", "--auth-type=legacy"], { stdio: "inherit" })npm config set @infragistics:registry <url>→Util.execFileSync("npm", ["config", "set", "@infragistics:registry", fullPackageRegistry]);
This keeps command semantics identical while eliminating shell string construction with tainted data.
| @@ -227,7 +227,7 @@ | ||
| const fullPackageRegistry = Util.sanitizeShellArg(config.igPackageRegistry); | ||
| try { | ||
| // tslint:disable-next-line:object-literal-sort-keys | ||
| Util.execSync(`npm whoami --registry=${fullPackageRegistry}`, { stdio: "pipe", encoding: "utf8" }); | ||
| Util.execFileSync("npm", ["whoami", `--registry=${fullPackageRegistry}`], { stdio: "pipe", encoding: "utf8" }); | ||
| } catch (error) { | ||
| // try registering the user: | ||
| Util.log( | ||
| @@ -250,12 +250,18 @@ | ||
| } | ||
|
|
||
| try { | ||
| Util.execSync( | ||
| `npm login --registry=${fullPackageRegistry} --scope=@infragistics --auth-type=legacy`, | ||
| Util.execFileSync( | ||
| "npm", | ||
| [ | ||
| "login", | ||
| `--registry=${fullPackageRegistry}`, | ||
| "--scope=@infragistics", | ||
| "--auth-type=legacy" | ||
| ], | ||
| { stdio: "inherit" } | ||
| ); | ||
| //make sure scope is configured: | ||
| Util.execSync(`npm config set @infragistics:registry ${fullPackageRegistry}`); | ||
| Util.execFileSync("npm", ["config", "set", "@infragistics:registry", fullPackageRegistry]); | ||
| return true; | ||
| } catch (error) { | ||
| Util.log(message, "red"); |
| @@ -1,5 +1,5 @@ | ||
| import chalk from "chalk"; | ||
| import { execSync, ExecSyncOptions, spawnSync, SpawnSyncOptions } from "child_process"; | ||
| import { execFileSync, execSync, ExecSyncOptions, spawnSync, SpawnSyncOptions } from "child_process"; | ||
| import * as fs from "fs"; | ||
| import * as glob from "glob"; | ||
| import * as path from "path"; | ||
| @@ -363,6 +363,30 @@ | ||
| } | ||
|
|
||
| /** | ||
| * Execute synchronous command without spawning a shell, using an explicit argument list. | ||
| * @param command Command to be executed | ||
| * @param args Arguments to pass to the command | ||
| * @param options Command options | ||
| * @throws {Error} On timeout or non-zero exit code. Error has 'status', 'signal', 'output', 'stdout', 'stderr' | ||
| */ | ||
| public static execFileSync(command: string, args: string[], options?: ExecSyncOptions) { | ||
| try { | ||
| return execFileSync(command, args, options); | ||
| } catch (error) { | ||
| // mirror execSync error handling for consistency | ||
| if (error.stderr && error.stderr.toString().endsWith() === "^C") { | ||
| return process.exit(); | ||
| } | ||
|
|
||
| if (error.status === 3221225786 || error.status > 128) { | ||
| return process.exit(); | ||
| } | ||
|
|
||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Execute synchronous command with options using spawnSync | ||
| * @param command Command to be executed | ||
| * NOTE: `spawn` without `shell` (unsafe) is **not** equivalent to `exec` & requires direct path to run the correct process on win, |
495a83d to
0aed238
Compare
| /* passes through */ | ||
| default: | ||
| args = ['uninstall', packageName, '--quiet', '--save']; | ||
| command = `${managerCommand} uninstall ${sanitizePackage} --quiet --save`; |
Check warning
Code scanning / CodeQL
Unsafe shell command constructed from library input Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 13 hours ago
General approach: Avoid building shell command strings that include tainted data and then passing them to execSync. Instead, pass the command and its arguments as separate parameters to child_process.spawnSync (or execFileSync), which does not invoke a shell and thus prevents shell interpretation of user‑controlled data. Where a helper (Util.execSync) currently wraps execSync, add a parallel helper that wraps spawnSync with an argument array, and update call sites that build dynamic commands from library inputs to use the array‑based helper.
Concrete fix:
-
In
packages/core/util/Util.ts, add a new static method,execFileSync, that accepts a command, an array of arguments, and optional options, and internally callsspawnSync. It should mirror the error‑handling logic ofexecSyncas closely as possible (status checks, SIGINT handling) so behavior stays consistent. -
In
packages/core/packages/PackageManager.ts, stop composing npm commands as single strings for the cases where arguments contain library input:- In
installPackages, instead ofcommand = \${managerCommand} install --quiet`andUtil.execSync(command, ...), buildconst args = ["install", "--quiet"];and callUtil.execFileSync(managerCommand, args, options)`. - In
removePackage, instead ofcommand = \${managerCommand} uninstall ${sanitizePackage} --quiet --save`, buildconst args = ["uninstall", sanitizePackage, "--quiet", "--save"];and callUtil.execFileSync(managerCommand, args, options)`. - In
addPackage, modifygetInstallCommandusage so that it returns args instead of a string, or, if we cannot modify that function here, reconstruct the equivalent array inaddPackage. Since we only see the call site, we’ll construct the array directly inaddPackageusingmanagerCommandandsanitizePackage.
- In
-
Keep
Util.sanitizeShellArgas‑is (it still adds a defense in depth against malformed or unexpected package names), but after the change it will no longer be relied upon for shell escaping, only for validating/normalizing input.
Imports: Util.ts already imports spawnSync and SpawnSyncOptions, so we can reuse those; no new external packages are needed.
| @@ -87,22 +87,15 @@ | ||
| public static async installPackages(verbose: boolean = false) { | ||
| const config = ProjectConfig.localConfig(); | ||
| if (!config.packagesInstalled) { | ||
| let command: string; | ||
| const managerCommand = this.getManager(); | ||
| const args = ["install", "--quiet"]; | ||
|
|
||
| switch (managerCommand) { | ||
| case "npm": | ||
| /* passes through */ | ||
| default: | ||
| command = `${managerCommand} install --quiet`; | ||
| break; | ||
| } | ||
| await this.flushQueue(false); | ||
| Util.log(`Installing ${managerCommand} packages`); | ||
| try { | ||
| // inherit the parent process' stdin so we can catch if an attempt to interrupt the process is made | ||
| // ignore stdout and stderr as they will output unnecessary text onto the console | ||
| Util.execSync(command, { stdio: ["inherit"], killSignal: "SIGINT" }); | ||
| Util.execFileSync(managerCommand, args, { stdio: ["inherit"], killSignal: "SIGINT" } as any); | ||
| Util.log(`Packages installed successfully`); | ||
| } catch (error) { | ||
| // ^C (SIGINT) produces status:3221225786 https://github.com/sass/node-sass/issues/1283#issuecomment-169450661 | ||
| @@ -122,19 +108,12 @@ | ||
| } | ||
|
|
||
| public static removePackage(packageName: string, verbose: boolean = false): boolean { | ||
| let command: string; | ||
| const managerCommand = this.getManager(); | ||
| const sanitizePackage = Util.sanitizeShellArg(packageName); | ||
| switch (managerCommand) { | ||
| case "npm": | ||
| /* passes through */ | ||
| default: | ||
| command = `${managerCommand} uninstall ${sanitizePackage} --quiet --save`; | ||
| break; | ||
| } | ||
| const args = ["uninstall", sanitizePackage, "--quiet", "--save"]; | ||
| try { | ||
| // tslint:disable-next-line:object-literal-sort-keys | ||
| Util.execSync(command, { stdio: "pipe", encoding: "utf8" }); | ||
| Util.execFileSync(managerCommand, args, { stdio: "pipe", encoding: "utf8" } as any); | ||
| } catch (error) { | ||
| Util.log(`Error uninstalling package ${sanitizePackage} with ${managerCommand}`); | ||
| if (verbose) { | ||
| @@ -150,10 +125,11 @@ | ||
| public static addPackage(packageName: string, verbose: boolean = false): boolean { | ||
| const managerCommand = this.getManager(); | ||
| const sanitizePackage = Util.sanitizeShellArg(packageName); | ||
| const command = this.getInstallCommand(managerCommand, sanitizePackage); | ||
| // Use argument array to avoid shell interpretation | ||
| const args = ["install", sanitizePackage, "--quiet", "--save"]; | ||
| try { | ||
| // tslint:disable-next-line:object-literal-sort-keys | ||
| Util.execSync(command, { stdio: "pipe", encoding: "utf8" }); | ||
| Util.execFileSync(managerCommand, args, { stdio: "pipe", encoding: "utf8" } as any); | ||
| } catch (error) { | ||
| Util.log(`Error installing package ${sanitizePackage} with ${managerCommand}`); | ||
| if (verbose) { |
| @@ -363,6 +363,42 @@ | ||
| } | ||
|
|
||
| /** | ||
| * Execute synchronous command without going through a shell, using an argument array. | ||
| * Mirrors the error and SIGINT handling of execSync. | ||
| * @param command Command to be executed | ||
| * @param args Arguments to pass to the command | ||
| * @param options Command options | ||
| */ | ||
| public static execFileSync(command: string, args: string[], options?: SpawnSyncOptions) { | ||
| const result = spawnSync(command, args, options); | ||
|
|
||
| // Handle interruption and non-zero exit codes similar to execSync's behavior | ||
| if (result.error) { | ||
| throw result.error; | ||
| } | ||
|
|
||
| if (result.stderr && result.stderr.toString().endsWith() === "^C") { | ||
| return process.exit(); | ||
| } | ||
|
|
||
| if (result.status === 3221225786 || (typeof result.status === "number" && result.status > 128)) { | ||
| return process.exit(); | ||
| } | ||
|
|
||
| if (typeof result.status === "number" && result.status !== 0) { | ||
| const error: any = new Error(`Command failed: ${command} ${args.join(" ")}`); | ||
| error.status = result.status; | ||
| error.signal = result.signal; | ||
| error.stdout = result.stdout; | ||
| error.stderr = result.stderr; | ||
| error.output = result.output; | ||
| throw error; | ||
| } | ||
|
|
||
| return result.stdout; | ||
| } | ||
|
|
||
| /** | ||
| * Execute synchronous command with options using spawnSync | ||
| * @param command Command to be executed | ||
| * NOTE: `spawn` without `shell` (unsafe) is **not** equivalent to `exec` & requires direct path to run the correct process on win, |
There was a problem hiding this comment.
Wouldn't be reverting if spawn was a valid option for npm commands cross-plat...
0edf0b2 to
4dc57d6
Compare
75f1b86 to
89f5958
Compare
1cdd9b5 to
cbb6502
Compare
|
|
||
| try { | ||
| Util.execSync( | ||
| `npm login --registry=${fullPackageRegistry} --scope=@infragistics --auth-type=legacy`, |
Check warning
Code scanning / CodeQL
Unsafe shell command constructed from library input Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 13 hours ago
In general, to fix this problem we should avoid constructing shell commands as single strings that are then interpreted by a shell. Instead, we should call npm with an API that accepts an executable and a list of arguments (for example, spawnSync or execFile) so that the registry URL is passed as a discrete argument and is not subject to shell parsing. This removes the need to rely on ad-hoc sanitization for correctness and security.
The best change with minimal functional impact is to update Util.execSync (the central helper) so that it can accept either a string or an array form, and internally dispatch to child_process.execSync when given a string (preserving all existing behaviour) or to child_process.spawnSync when given an executable and argument array. Then we adjust the two npm calls in PackageManager.ensureRegistryUser to use the safer array form: Util.execSync(["npm", "whoami", --registry=${fullPackageRegistry}], options) and Util.execSync(["npm", "login", --registry=${fullPackageRegistry}, "--scope=@infragistics", "--auth-type=legacy"], { stdio: "inherit" }), plus the npm config set call similarly. This keeps the commands logically identical while avoiding shell parsing of fullPackageRegistry.
Concretely:
- In
packages/core/util/Util.ts, change the signature ofUtil.execSyncto acceptcommand: string | string[]. Inside, iftypeof command === "string", callexecSync(command, options)as before; if it is an array, treatcommand[0]as the executable andcommand.slice(1)as its arguments, and invokespawnSyncwith those, mimickingexecSync’s behaviour and error handling as closely as practical (throw on non-zero exit, surface stdout/stderr, respectoptions.stdioto the extent possible). - Preserve the existing error-handling logic for the string path, and for the array path, add analogous checks on
result.statusandresult.stderr, throwing anErrorwhen the spawned process fails. - In
packages/core/packages/PackageManager.ts, update the threeUtil.execSynccalls inensureRegistryUserto use the array form described above. We keep the existingfullPackageRegistrysanitizer and log messages, but the value is no longer interpolated into a shell string.
This keeps overall behaviour the same from the caller’s perspective while structurally eliminating the unsafe shell command construction for these flows.
| @@ -227,7 +227,10 @@ | ||
| const fullPackageRegistry = Util.sanitizeShellArg(config.igPackageRegistry); | ||
| try { | ||
| // tslint:disable-next-line:object-literal-sort-keys | ||
| Util.execSync(`npm whoami --registry=${fullPackageRegistry}`, { stdio: "pipe", encoding: "utf8" }); | ||
| Util.execSync( | ||
| ["npm", "whoami", `--registry=${fullPackageRegistry}`], | ||
| { stdio: "pipe", encoding: "utf8" } | ||
| ); | ||
| } catch (error) { | ||
| // try registering the user: | ||
| Util.log( | ||
| @@ -251,11 +254,17 @@ | ||
|
|
||
| try { | ||
| Util.execSync( | ||
| `npm login --registry=${fullPackageRegistry} --scope=@infragistics --auth-type=legacy`, | ||
| ["npm", "login", | ||
| `--registry=${fullPackageRegistry}`, | ||
| "--scope=@infragistics", | ||
| "--auth-type=legacy" | ||
| ], | ||
| { stdio: "inherit" } | ||
| ); | ||
| //make sure scope is configured: | ||
| Util.execSync(`npm config set @infragistics:registry ${fullPackageRegistry}`); | ||
| Util.execSync( | ||
| ["npm", "config", "set", `@infragistics:registry`, fullPackageRegistry] | ||
| ); | ||
| return true; | ||
| } catch (error) { | ||
| Util.log(message, "red"); |
| return false; | ||
| } | ||
| } else { | ||
| Util.execSync(`npm config set @infragistics:registry ${fullPackageRegistry}`); |
Check warning
Code scanning / CodeQL
Unsafe shell command constructed from library input Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 13 hours ago
In general, to fix this issue you should stop concatenating potentially untrusted input into a single string command executed by a shell. Instead, call child_process.execFileSync/spawnSync (or equivalent) with the command name and a list of arguments so that the shell does not interpret special characters. If you must use a shell, then you should properly quote/escape untrusted arguments using a robust library (for example shell-quote) rather than a custom sanitizer.
For this specific case, the commands being run are plain npm commands without pipes or redirections, so they can be safely executed without a shell. A minimal change that preserves existing functionality is:
- Add a new helper method in
Util(e.g.execFileSyncSafe) that wrapschild_process.spawnSync(already imported) to execute a command and arguments array without a shell, reusing the existing error-handling semantics ofexecSyncas closely as practical. - Update the three problematic calls in
PackageManager.ensureRegistryUser:Util.execSync(\npm whoami --registry=${fullPackageRegistry}`, ...)`Util.execSync(\npm login --registry=${fullPackageRegistry} --scope=@Infragistics --auth-type=legacy`, ...)`Util.execSync(\npm config set @Infragistics:registry ${fullPackageRegistry}`);`
to instead call the new helper with explicit argument arrays:Util.execFileSyncSafe("npm", ["whoami",--registry=${fullPackageRegistry}], {...});Util.execFileSyncSafe("npm", ["login",--registry=${fullPackageRegistry}, "--scope=@infragistics", "--auth-type=legacy"], {...});Util.execFileSyncSafe("npm", ["config", "set", "@infragistics:registry", fullPackageRegistry]);
Because spawnSync is already imported in Util.ts, no new imports are needed. The new helper should throw an Error in a way consistent with execSync when the child process fails (non‑zero exit code), so callers like ensureRegistryUser keep working as before.
| @@ -227,7 +227,7 @@ | ||
| const fullPackageRegistry = Util.sanitizeShellArg(config.igPackageRegistry); | ||
| try { | ||
| // tslint:disable-next-line:object-literal-sort-keys | ||
| Util.execSync(`npm whoami --registry=${fullPackageRegistry}`, { stdio: "pipe", encoding: "utf8" }); | ||
| Util.execFileSyncSafe("npm", ["whoami", `--registry=${fullPackageRegistry}`], { stdio: "pipe", encoding: "utf8" }); | ||
| } catch (error) { | ||
| // try registering the user: | ||
| Util.log( | ||
| @@ -250,12 +250,18 @@ | ||
| } | ||
|
|
||
| try { | ||
| Util.execSync( | ||
| `npm login --registry=${fullPackageRegistry} --scope=@infragistics --auth-type=legacy`, | ||
| Util.execFileSyncSafe( | ||
| "npm", | ||
| [ | ||
| "login", | ||
| `--registry=${fullPackageRegistry}`, | ||
| "--scope=@infragistics", | ||
| "--auth-type=legacy" | ||
| ], | ||
| { stdio: "inherit" } | ||
| ); | ||
| //make sure scope is configured: | ||
| Util.execSync(`npm config set @infragistics:registry ${fullPackageRegistry}`); | ||
| Util.execFileSyncSafe("npm", ["config", "set", "@infragistics:registry", fullPackageRegistry]); | ||
| return true; | ||
| } catch (error) { | ||
| Util.log(message, "red"); |
| @@ -363,6 +363,43 @@ | ||
| } | ||
|
|
||
| /** | ||
| * Execute a command without involving a shell, using argument array. | ||
| * Mirrors execSync error handling semantics as closely as possible. | ||
| * @param command Command to be executed | ||
| * @param args Arguments to pass to the command | ||
| * @param options Command options | ||
| */ | ||
| public static execFileSyncSafe(command: string, args: string[], options?: SpawnSyncOptions) { | ||
| const result = spawnSync(command, args, options); | ||
|
|
||
| if (result.error) { | ||
| throw result.error; | ||
| } | ||
|
|
||
| if (result.signal || result.status && result.status !== 0) { | ||
| const error: any = new Error(`Command failed: ${command} ${args.join(" ")}`); | ||
| error.status = result.status; | ||
| error.signal = result.signal; | ||
| error.stdout = result.stdout; | ||
| error.stderr = result.stderr; | ||
| error.output = result.output; | ||
|
|
||
| // Mimic existing execSync interruption handling: | ||
| if (error.stderr && error.stderr.toString().endsWith() === "^C") { | ||
| return process.exit(); | ||
| } | ||
|
|
||
| if (error.status === 3221225786 || error.status > 128) { | ||
| return process.exit(); | ||
| } | ||
|
|
||
| throw error; | ||
| } | ||
|
|
||
| return result.stdout; | ||
| } | ||
|
|
||
| /** | ||
| * Execute synchronous command with options using spawnSync | ||
| * @param command Command to be executed | ||
| * NOTE: `spawn` without `shell` (unsafe) is **not** equivalent to `exec` & requires direct path to run the correct process on win, |
Closes #1512 .
Additional information related to this pull request:
See issue details here: #1512 (comment)
reverted most of the changes from resolve code scanning vulnerability alerts #1331 and fix: command injection vulnerabilities in PackageManager and start command #1438 swapping
execwithspawnbecause:spawnis not equivalent toexec- it does not throw for bad execution exit, in fact it barely throws for anything other than call configuration, so it fails 'silently' for our use cases and required handling rework where calledspawnon Windows can't runnpmsince it's a script withoutshellevaluation option; used to be able to runnpm.cmdbut that's also not available since node@24+; I've checked and most secure calls intospawnthat want to call in some node package rely onprocess.execPathand the path to the script (essentially callingnode path/to/file.js) which could work for installed packages (thinktsc), but no reliable way to resolve thenpmpath itselfDropped extra param where it wasn't really needed and added a sanitize function
Swapped one older call using
spawnw/npm.cmdsince that no longer works with node@24+Note: CodeQL doesn't pick up the sanitize function out-of-the-box and my customization attempts have failed thus far - will leave for a separate PR. In fact, might re-work the command usage entirely to be async first and switch to writing to
package.jsoninstead with a single static command call at the end.