CVE-2022-22984: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') (CWE-78)
value: The package snyk before 1.1064.0; the package snyk-mvn-plugin before 2.31.3; the package snyk-gradle-plugin before 3.24.5; the package @snyk/snyk-cocoapods-plugin before 2.5.3; the package snyk-sbt-plugin before 2.16.2; the package snyk-python-plugin before 1.24.2; the package snyk-docker-plugin before 5.6.5; the package @snyk/snyk-hex-plugin before 1.1.6 are vulnerable to Command Injection due to an incomplete fix for [CVE-2022-40764](https://security.snyk.io/vuln/SNYK-JS-SNYK-3037342). A successful exploit allows attackers to run arbitrary commands on the host system where the Snyk CLI is installed by passing in crafted command line flags. In order to exploit this vulnerability, a user would have to execute the snyk test command on untrusted files. In most cases, an attacker positioned to control the command line arguments to the Snyk CLI would already be positioned to execute arbitrary commands. However, this could be abused in specific scenarios, such as continuous integration pipelines, where developers can control the arguments passed to the Snyk CLI to leverage this component as part of a wider attack against an integration/build pipeline. This issue has been addressed in the latest Snyk Docker images available at https://hub.docker.com/r/snyk/snyk as of 2022-11-29. Images downloaded and built prior to that date should be updated. The issue has also been addressed in the Snyk TeamCity CI/CD plugin as of version v20221130.093605.
CWE description: The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.
CVSS v3 base score: 6.3
Published: 2022-11-30T13:15Z
Repository: https://github.com/snyk/snyk-gradle-plugin
Language: TypeScript
Files changed: .snyk, lib/index.ts, lib/sub-process.ts, package.json, test/functional/gradle-plugin.spec.ts
Commit date: 2022-11-14 13:49:11 +0000
Commit message: fix: escape child process arguments

Diff: diff --git a/.snyk b/.snyk
new file mode 100644
index 0000000..8a2d8d4
--- /dev/null
+++ b/.snyk
@@ -0,0 +1,10 @@
+# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
+version: v1.25.0
+# ignores vulnerabilities until expiry date; change duration by modifying expiry date
+ignore:
+  'snyk:lic:npm:shescape:MPL-2.0':
+    - '*':
+        reason: Snyk CLI handles MPL-2.0 by appending dependency to snyk --about
+        expires: 2122-12-14T13:33:15.042Z
+        created: 2022-11-14T13:33:15.045Z
+patch: {}
diff --git a/lib/index.ts b/lib/index.ts
index 0fd7117..fdb4319 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -35,13 +35,10 @@ export function debugLog(s: string) {
     logger = debugModule('snyk-gradle-plugin');
   }
   logger(s);
 }
 
-const isWin = /^win/.test(os.platform());
-const quot = isWin ? '"' : "'";
-
 const cannotResolveVariantMarkers = [
   'Cannot choose between the following',
   'Could not select value from candidates',
   'Unable to find a matching variant of project',
 ];
@@ -113,10 +110,11 @@ export async function inspect(
     options = { dev: false };
   }
   let subProject = (options as api.SingleSubprojectInspectOptions).subProject;
   if (subProject) {
     subProject = subProject.trim();
+    (options as api.SingleSubprojectInspectOptions).subProject = subProject;
   }
   const plugin: api.PluginMetadata = {
     name: 'bundled:gradle',
     runtime: 'unknown',
     targetFile: targetFileFilteredForCompatibility(targetFile),
@@ -669,17 +667,10 @@ function getCommand(root: string, targetFile: string) {
     return quotLocal + pathToWrapper + quotLocal;
   }
   return 'gradle';
 }
 
-export function formatArgWithWhiteSpace(arg: string): string {
-  if (/\s/.test(arg)) {
-    return quot + arg + quot;
-  }
-  return arg;
-}
-
 function buildArgs(
   root: string,
   targetFile: string | null,
   initGradlePath: string,
   options: Options,
@@ -690,35 +681,29 @@ function buildArgs(
     : 'snykResolvedDepsJson';
 
   args.push(taskName, '-q');
 
   if (targetFile) {
-    if (!fs.existsSync(path.resolve(root, targetFile))) {
-      throw new Error('File not found: "' + targetFile + '"');
+    const resolvedTargetFilePath = path.resolve(root, targetFile);
+    if (!fs.existsSync(resolvedTargetFilePath)) {
+      throw new Error('File not found: "' + resolvedTargetFilePath + '"');
     }
     args.push('--build-file');
 
-    const formattedTargetFile = formatArgWithWhiteSpace(targetFile);
-    args.push(formattedTargetFile);
+    args.push(resolvedTargetFilePath);
   }
 
   // Arguments to init script are supplied as properties: https://stackoverflow.com/a/48370451
   if (options['configuration-matching']) {
-    args.push(
-      `-Pconfiguration=${quot}${options['configuration-matching']}${quot}`,
-    );
+    args.push(`-Pconfiguration=${options['configuration-matching']}`);
   }
   if (options['configuration-attributes']) {
-    args.push(
-      `-PconfAttr=${quot}${options['configuration-attributes']}${quot}`,
-    );
+    args.push(`-PconfAttr=${options['configuration-attributes']}`);
   }
 
   if (options.initScript) {
-    const formattedInitScript = formatArgWithWhiteSpace(
-      path.resolve(options.initScript),
-    );
+    const formattedInitScript = path.resolve(options.initScript);
     args.push('--init-script', formattedInitScript);
   }
 
   if (!options.daemon) {
     args.push('--no-daemon');
@@ -736,11 +721,11 @@ function buildArgs(
 
   if (!api.isMultiSubProject(options)) {
     args.push('-PonlySubProject=' + (options.subProject || '.'));
   }
 
-  args.push('-I ' + initGradlePath);
+  args.push('-I', initGradlePath);
 
   if (options.args) {
     args.push(...options.args);
   }
 
@@ -749,20 +734,20 @@ function buildArgs(
   // but we are handling it to support the legacy setups.
   args.forEach((a, i) => {
     // Transform --configuration=foo
     args[i] = a.replace(
       /^--configuration[= ]([a-zA-Z_]+)/,
-      `-Pconfiguration=${quot}^$1$$${quot}`,
+      `-Pconfiguration=^$1$$`,
     );
     // Transform --configuration foo
     if (a === '--configuration') {
-      args[i] = `-Pconfiguration=${quot}^${args[i + 1]}$${quot}`;
+      args[i] = `-Pconfiguration=^${args[i + 1]}$`;
       args[i + 1] = '';
     }
   });
 
-  return args;
+  return args.filter(Boolean);
 }
 
 export const exportsForTests = {
   buildArgs,
   extractJsonFromScriptOutput,
diff --git a/lib/sub-process.ts b/lib/sub-process.ts
index d5ec056..6003780 100644
--- a/lib/sub-process.ts
+++ b/lib/sub-process.ts
@@ -1,7 +1,8 @@
 import * as childProcess from 'child_process';
 import debugModule = require('debug');
+import { quoteAll } from 'shescape';
 
 const debugLogging = debugModule('snyk-gradle-plugin');
 
 // Executes a subprocess. Resolves successfully with stdout contents if the exit code is 0.
 export function execute(
@@ -12,10 +13,11 @@ export function execute(
 ): Promise<string> {
   const spawnOptions: childProcess.SpawnOptions = { shell: true };
   if (options && options.cwd) {
     spawnOptions.cwd = options.cwd;
   }
+  args = quoteAll(args, spawnOptions);
 
   return new Promise((resolve, reject) => {
     let stdout = '';
     let stderr = '';
 
diff --git a/package.json b/package.json
index 6bbeaef..f13f71c 100644
--- a/package.json
+++ b/package.json
@@ -49,10 +49,11 @@
     "@types/debug": "^4.1.4",
     "chalk": "^3.0.0",
     "debug": "^4.1.1",
     "p-map": "^4.0.0",
     "packageurl-js": "^1.0.0",
+    "shescape": "1.6.1",
     "tmp": "0.2.1",
     "tslib": "^2.0.0"
   },
   "engines": {
     "node": ">=16"
diff --git a/test/functional/gradle-plugin.spec.ts b/test/functional/gradle-plugin.spec.ts
index 4f4f822..7bf124c 100644
--- a/test/functional/gradle-plugin.spec.ts
+++ b/test/functional/gradle-plugin.spec.ts
@@ -1,10 +1,6 @@
 import { exportsForTests as testableMethods } from '../../lib';
-import * as os from 'os';
-
-const isWin = /^win/.test(os.platform());
-const quot = isWin ? '"' : "'";
 
 const JEST_TIMEOUT = 15000;
 
 describe('Gradle Plugin', () => {
   it('check build args (plain console output)', () => {
@@ -14,11 +10,12 @@ describe('Gradle Plugin', () => {
       '-q',
       '--no-daemon',
       '-Dorg.gradle.parallel=',
       '-Dorg.gradle.console=plain',
       '-PonlySubProject=.',
-      '-I /tmp/init.gradle',
+      '-I',
+      '/tmp/init.gradle',
     ]);
   });
 
   it('check build args with array (new configuration arg)', async () => {
     const result = testableMethods.buildArgs('.', null, '/tmp/init.gradle', {
@@ -26,16 +23,17 @@ describe('Gradle Plugin', () => {
       args: ['--build-file', 'build.gradle'],
     });
     expect(result).toEqual([
       'snykResolvedDepsJson',
       '-q',
-      `-Pconfiguration=${quot}confRegex${quot}`,
+      `-Pconfiguration=confRegex`,
       '--no-daemon',
       '-Dorg.gradle.parallel=',
       '-Dorg.gradle.console=plain',
       '-PonlySubProject=.',
-      '-I /tmp/init.gradle',
+      '-I',
+      '/tmp/init.gradle',
       '--build-file',
       'build.gradle',
     ]);
   });
 
@@ -46,15 +44,16 @@ describe('Gradle Plugin', () => {
       args: ['--build-file', 'build.gradle'],
     });
     expect(result).toEqual([
       'snykResolvedDepsJson',
       '-q',
-      `-Pconfiguration=${quot}confRegex${quot}`,
+      `-Pconfiguration=confRegex`,
       '-Dorg.gradle.parallel=',
       '-Dorg.gradle.console=plain',
       '-PonlySubProject=.',
-      '-I /tmp/init.gradle',
+      '-I',
+      '/tmp/init.gradle',
       '--build-file',
       'build.gradle',
     ]);
   });
 
@@ -67,14 +66,15 @@ describe('Gradle Plugin', () => {
       '-q',
       '--no-daemon',
       '-Dorg.gradle.parallel=',
       '-Dorg.gradle.console=plain',
       '-PonlySubProject=.',
-      '-I /tmp/init.gradle',
+      '-I',
+      '/tmp/init.gradle',
       '--build-file',
       'build.gradle',
-      `-Pconfiguration=${quot}^compile$${quot}`,
+      `-Pconfiguration=^compile$`,
     ]);
   });
 
   it(
     'check build args with scan all subprojects',
@@ -87,15 +87,15 @@ describe('Gradle Plugin', () => {
         'snykResolvedDepsJson',
         '-q',
         '--no-daemon',
         '-Dorg.gradle.parallel=',
         '-Dorg.gradle.console=plain',
-        '-I /tmp/init.gradle',
+        '-I',
+        '/tmp/init.gradle',
         '--build-file',
         'build.gradle',
-        `-Pconfiguration=${quot}^compile$${quot}`,
-        '', // this is a harmless artifact of argument transformation
+        `-Pconfiguration=^compile$`,
       ]);
     },
     JEST_TIMEOUT,
   );
 });

Vulnerable code: const isWin = /^win/.test(os.platform());
const quot = isWin ? '"' : "'";

export function formatArgWithWhiteSpace(arg: string): string {
  if (/\s/.test(arg)) {
    return quot + arg + quot;
  }
  return arg;
}

    if (!fs.existsSync(path.resolve(root, targetFile))) {
      throw new Error('File not found: "' + targetFile + '"');
    const formattedTargetFile = formatArgWithWhiteSpace(targetFile);
    args.push(formattedTargetFile);
    args.push(
      `-Pconfiguration=${quot}${options['configuration-matching']}${quot}`,
    );
    args.push(
      `-PconfAttr=${quot}${options['configuration-attributes']}${quot}`,
    );
    const formattedInitScript = formatArgWithWhiteSpace(
      path.resolve(options.initScript),
    );
  args.push('-I ' + initGradlePath);
      `-Pconfiguration=${quot}^$1$$${quot}`,
      args[i] = `-Pconfiguration=${quot}^${args[i + 1]}$${quot}`;
  return args;
import * as os from 'os';

const isWin = /^win/.test(os.platform());
const quot = isWin ? '"' : "'";
      '-I /tmp/init.gradle',
      `-Pconfiguration=${quot}confRegex${quot}`,
      '-I /tmp/init.gradle',
      `-Pconfiguration=${quot}confRegex${quot}`,
      '-I /tmp/init.gradle',
      '-I /tmp/init.gradle',
      `-Pconfiguration=${quot}^compile$${quot}`,
        '-I /tmp/init.gradle',
        `-Pconfiguration=${quot}^compile$${quot}`,
        '', // this is a harmless artifact of argument transformation
Fixed code: # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.25.0
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore:
  'snyk:lic:npm:shescape:MPL-2.0':
    - '*':
        reason: Snyk CLI handles MPL-2.0 by appending dependency to snyk --about
        expires: 2122-12-14T13:33:15.042Z
        created: 2022-11-14T13:33:15.045Z
patch: {}
    (options as api.SingleSubprojectInspectOptions).subProject = subProject;
    const resolvedTargetFilePath = path.resolve(root, targetFile);
    if (!fs.existsSync(resolvedTargetFilePath)) {
      throw new Error('File not found: "' + resolvedTargetFilePath + '"');
    args.push(resolvedTargetFilePath);
    args.push(`-Pconfiguration=${options['configuration-matching']}`);
    args.push(`-PconfAttr=${options['configuration-attributes']}`);
    const formattedInitScript = path.resolve(options.initScript);
  args.push('-I', initGradlePath);
      `-Pconfiguration=^$1$$`,
      args[i] = `-Pconfiguration=^${args[i + 1]}$`;
  return args.filter(Boolean);
import { quoteAll } from 'shescape';
  args = quoteAll(args, spawnOptions);
    "shescape": "1.6.1",
      '-I',
      '/tmp/init.gradle',
      `-Pconfiguration=confRegex`,
      '-I',
      '/tmp/init.gradle',
      `-Pconfiguration=confRegex`,
      '-I',
      '/tmp/init.gradle',
      '-I',
      '/tmp/init.gradle',
      `-Pconfiguration=^compile$`,
        '-I',
        '/tmp/init.gradle',
        `-Pconfiguration=^compile$`,