VYPR
Critical severityOSV Advisory· Published Jan 13, 2023· Updated Apr 4, 2025

CVE-2022-21191

CVE-2022-21191

Description

global-modules-path before 3.0.0 is vulnerable to command injection via unsanitized input to the getPath function, allowing arbitrary command execution.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

global-modules-path before 3.0.0 is vulnerable to command injection via unsanitized input to the getPath function, allowing arbitrary command execution.

The getPath function in the global-modules-path npm package prior to version 3.0.0 fails to sanitize user-supplied package names or executable names before passing them to shell commands. This allows an attacker to inject arbitrary shell commands through specially crafted input, as the library uses child_process.execSync which interprets shell metacharacters [1][2][4].

An attacker can exploit this by providing a malicious string as the package name or executable name argument to getPath. For example, the proof-of-concept from Snyk demonstrates using "& touch JHU" as the package name, which results in execution of the injected command [4]. No authentication is required; the vulnerability is triggered whenever a Node.js application calls getPath with untrusted input.

Successful exploitation enables arbitrary command execution in the context of the Node.js process. This can lead to full system compromise, including data exfiltration, installation of malware, or lateral movement within the network [1][4].

The vulnerability is fixed in version 3.0.0 by replacing execSync with spawnSync, which does not invoke a shell and thus prevents command injection [2]. Users are advised to upgrade to the latest version immediately.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
global-modules-pathnpm
< 3.0.03.0.0

Affected products

2

Patches

1
edbdaff077ea

fix: do not allow command injection

2 files changed · +71 51
  • lib/index.js+23 4 modified
    @@ -25,9 +25,26 @@ const getNpmExecutable = (platform) => {
     	return npmExecutableName;
     };
     
    +const spawnSyncWrapper = (command, commandArgs) => {
    +	const result = childProcess.spawnSync(command, commandArgs);
    +	if (!result) {
    +		return null;
    +	}
    +
    +	if (result.error) {
    +		throw result.error;
    +	}
    +
    +	if (result.stdout) {
    +		return result.stdout.toString().trim();
    +	}
    +
    +	return null;
    +};
    +
     const getNpmPrefix = (pathToNpm) => {
     	try {
    -		const npmPrefixStdout = childProcess.execSync(`${pathToNpm} config get prefix`);
    +		const npmPrefixStdout = spawnSyncWrapper(pathToNpm, ["config", "get", "prefix"]);
     		return npmPrefixStdout && npmPrefixStdout.toString().trim();
     	} catch (err) {
     		console.error(err.message);
    @@ -49,6 +66,7 @@ const getPathFromNpmConfig = (platform, packageName) => {
     
     		const packagePath = path.join(nodeModulesPath, packageName);
     		const verifiedPath = getVerifiedPath(packagePath, packageName);
    +
     		if (verifiedPath) {
     			return verifiedPath;
     		}
    @@ -107,7 +125,8 @@ const getVerifiedPath = (suggestedPath, packageName) => {
     
     const getPathFromExecutableNameOnWindows = (packageName, executableName) => {
     	try {
    -		const whereResult = (childProcess.execSync(`where ${executableName}`) || "").toString().split("\n");
    +		const whereResult = (spawnSyncWrapper("where", executableName) || "").split("\n");
    +
     		for (const line of whereResult) {
     			const pathToExecutable = line && line.trim();
     
    @@ -145,8 +164,8 @@ const getPathFromExecutableNameOnNonWindows = (packageName, executableName) => {
     
     		// whichResult: /usr/local/nvm/versions/node/v4.2.1/bin/mobile-cli-lib
     		// lsLResult: lrwxrwxrwx 1 rvladimirov rvladimirov 52 Oct 20 14:51 /usr/local/nvm/versions/node/v4.2.1/bin/mobile-cli-lib -> ../lib/node_modules/mobile-cli-lib/bin/common-lib.js
    -		const whichResult = (childProcess.execSync(`which ${executableName}`) || "").toString().trim(),
    -			lsLResult = (childProcess.execSync(`ls -l \`which ${executableName}\``) || "").toString().trim();
    +		const whichResult = spawnSyncWrapper("which", [executableName]);
    +		const lsLResult = spawnSyncWrapper("ls", ["-l", whichResult]);
     
     		if (whichResult && lsLResult) {
     			const regex = new RegExp(`${whichResult}\\s+->\\s+(.*?)$`),
    
  • test/index.js+48 47 modified
    @@ -12,7 +12,7 @@ let processWrapper = require("../lib/process-wrapper");
     let fs = require("fs"),
     	childProcess = require("child_process");
     
    -const originalExecSync = childProcess.execSync;
    +const originalspawnSync = childProcess.spawnSync;
     const originalConsoleError = console.error;
     const originalExistsSync = fs.existsSync;
     const originalReadFileSync = fs.readFileSync;
    @@ -21,7 +21,7 @@ describe("getPath", () => {
     
     	afterEach(() => {
     		processWrapper.getProcessPlatform = () => process.platform;
    -		childProcess.execSync = originalExecSync;
    +		childProcess.spawnSync = originalspawnSync;
     		console.error = originalConsoleError;
     		fs.existsSync = originalExistsSync;
     		fs.readFileSync = originalReadFileSync;
    @@ -41,9 +41,9 @@ describe("getPath", () => {
     					const expectedErrorMessage = "npm throws";
     					let errors = [];
     					console.error = (message) => errors.push(message);
    -					childProcess.execSync = (command) => {
    -						if (command.indexOf("get prefix") !== -1) {
    -							throw new Error(expectedErrorMessage);
    +					childProcess.spawnSync = (command, commandArgs) => {
    +						if (commandArgs.indexOf("get") !== -1 && commandArgs.indexOf("prefix") !== -1) {
    +							return { error: new Error(expectedErrorMessage) };
     						}
     					};
     
    @@ -59,7 +59,7 @@ describe("getPath", () => {
     
     					fs.existsSync = () => false;
     
    -					childProcess.execSync = (command) => {
    +					childProcess.spawnSync = (command) => {
     						if (command.indexOf("get prefix") !== -1) {
     							return npmConfigPrefix;
     						}
    @@ -75,7 +75,7 @@ describe("getPath", () => {
     					const packageName = "test1",
     						executableName = "test1.js";
     
    -					childProcess.execSync = () => null;
    +					childProcess.spawnSync = () => null;
     
     					const result = index.getPath(packageName, executableName);
     					assert.deepEqual(result, null);
    @@ -87,9 +87,10 @@ describe("getPath", () => {
     	const getCorrectResultFromNpmPrefix = (npmConfigPrefix, packageName) => {
     		fs.existsSync = () => true;
     		fs.readFileSync = () => JSON.stringify({ name: packageName });
    -		childProcess.execSync = (command) => {
    -			if (command.indexOf("get prefix") !== -1) {
    -				return npmConfigPrefix;
    +		fs.realpathSync = (p) => p; 
    +		childProcess.spawnSync = (command, commandArgs) => {
    +			if (commandArgs.indexOf("get") !== -1 && commandArgs.indexOf("prefix") !== -1) {
    +				return { stdout: npmConfigPrefix };
     			}
     		};
     
    @@ -105,8 +106,8 @@ describe("getPath", () => {
     		describe("works correctly when npm prefix is used", () => {
     			it("uses npm.cmd for npm executable", () => {
     				let commands = [];
    -				childProcess.execSync = (command) => {
    -					commands.push(command);
    +				childProcess.spawnSync = (command, commandArgs) => {
    +					commands.push(`${command} ${(commandArgs || []).join(" ")}`);
     				};
     
     				index.getPath("test1");
    @@ -141,9 +142,9 @@ describe("getPath", () => {
     
     				fs.existsSync = () => true;
     
    -				childProcess.execSync = (command) => {
    +				childProcess.spawnSync = (command) => {
     					if (command.indexOf("where") !== -1) {
    -						throw new Error(expectedErrorMessage);
    +						return { error: new Error(expectedErrorMessage) };
     					}
     
     					return null;
    @@ -163,9 +164,9 @@ describe("getPath", () => {
     
     				fs.existsSync = () => true;
     
    -				childProcess.execSync = (command) => {
    +				childProcess.spawnSync = (command) => {
     					if (command.indexOf("where") !== -1) {
    -						return whereResult;
    +						return { stdout: whereResult };
     					}
     
     					return null;
    @@ -194,9 +195,9 @@ describe("getPath", () => {
     
     				fs.existsSync = (pathToCheck) => pathToCheck !== path.join(executableDirName, "node_modules", packageName);
     
    -				childProcess.execSync = (command) => {
    +				childProcess.spawnSync = (command) => {
     					if (command.indexOf("where") !== -1) {
    -						return whereResult;
    +						return { stdout: whereResult };
     					}
     
     					return null;
    @@ -233,9 +234,9 @@ describe("getPath", () => {
     
     				fs.existsSync = (pathToCheck) => pathToCheck !== path.join(executableDirName, "node_modules", packageName);
     
    -				childProcess.execSync = (command) => {
    +				childProcess.spawnSync = (command) => {
     					if (command.indexOf("where") !== -1) {
    -						return whereResult;
    +						return { stdout: whereResult };
     					}
     
     					return null;
    @@ -284,9 +285,9 @@ describe("getPath", () => {
     					return "";
     				};
     
    -				childProcess.execSync = (command) => {
    +				childProcess.spawnSync = (command) => {
     					if (command.indexOf("where") !== -1) {
    -						return whereResult;
    +						return { stdout: whereResult };
     					}
     
     					return null;
    @@ -316,9 +317,9 @@ describe("getPath", () => {
     					return "";
     				};
     
    -				childProcess.execSync = (command) => {
    +				childProcess.spawnSync = (command) => {
     					if (command.indexOf("where") !== -1) {
    -						return whereResult;
    +						return { stdout: whereResult };
     					}
     
     					return null;
    @@ -354,9 +355,9 @@ describe("getPath", () => {
     					return "";
     				};
     
    -				childProcess.execSync = (command) => {
    +				childProcess.spawnSync = (command) => {
     					if (command.indexOf("where") !== -1) {
    -						return whereResult;
    +						return { stdout: whereResult };
     					}
     
     					return null;
    @@ -380,8 +381,8 @@ describe("getPath", () => {
     
     			it("uses npm for npm executable", () => {
     				let commands = [];
    -				childProcess.execSync = (command) => {
    -					commands.push(command);
    +				childProcess.spawnSync = (command, commandArgs) => {
    +					commands.push(`${command} ${(commandArgs || []).join(" ")}`);
     				};
     
     				index.getPath("test1");
    @@ -416,7 +417,7 @@ describe("getPath", () => {
     
     					fs.existsSync = () => true;
     
    -					childProcess.execSync = (command) => {
    +					childProcess.spawnSync = (command) => {
     						if (command.indexOf("which") !== -1) {
     							throw new Error(expectedErrorMessage);
     						}
    @@ -440,9 +441,9 @@ describe("getPath", () => {
     
     					fs.existsSync = () => true;
     
    -					childProcess.execSync = (command) => {
    -						if (command.indexOf("ls -l") !== -1) {
    -							throw new Error(expectedErrorMessage);
    +					childProcess.spawnSync = (command) => {
    +						if (command === "ls") {
    +							return { error: new Error(expectedErrorMessage) };
     						}
     
     						return null;
    @@ -479,14 +480,14 @@ describe("getPath", () => {
     							return filePath;
     						};
     
    -						childProcess.execSync = (command) => {
    +						childProcess.spawnSync = (command) => {
     
    -							if (command.indexOf("ls -l") !== -1) {
    -								return lsLResult;
    +							if (command === "ls") {
    +								return { stdout: lsLResult };
     							}
     
    -							if (command.indexOf("which") !== -1) {
    -								return whichResult;
    +							if (command == "which") {
    +								return { stdout: whichResult };
     							}
     
     							return null;
    @@ -544,14 +545,14 @@ describe("getPath", () => {
     							return filePath.indexOf("package.json") !== -1;
     						};
     
    -						childProcess.execSync = (command) => {
    +						childProcess.spawnSync = (command) => {
     
    -							if (command.indexOf("ls -l") !== -1) {
    -								return lsLResult;
    +							if (command === "ls") {
    +								return { stdout: lsLResult };
     							}
     
    -							if (command.indexOf("which") !== -1) {
    -								return whichResult;
    +							if (command == "which") {
    +								return { stdout: whichResult };
     							}
     
     							return null;
    @@ -582,14 +583,14 @@ describe("getPath", () => {
     							return filePath.indexOf("package.json") !== -1;
     						};
     
    -						childProcess.execSync = (command) => {
    +						childProcess.spawnSync = (command) => {
     
    -							if (command.indexOf("ls -l") !== -1) {
    -								return lsLResult;
    +							if (command === "ls") {
    +								return { stdout: lsLResult };
     							}
     
    -							if (command.indexOf("which") !== -1) {
    -								return whichResult;
    +							if (command == "which") {
    +								return { stdout: whichResult };
     							}
     
     							return null;
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.