CVE-2021-34078: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') (CWE-78)
value: lifion-verify-dependencies through 1.1.0 is vulnerable to OS command injection via a crafted dependency name on the scanned project's package.json file.
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: 8.8
Published: 2022-06-02T14:15Z
Repository: https://github.com/lifion/lifion-verify-deps
Language: JSON
Files changed: lib/index.js, lib/index.test.js, package-lock.json, package.json
Commit date: 2021-02-24 16:00:22 -0500
Commit message: Add validation for npm module name

Diff: diff --git a/lib/index.js b/lib/index.js
index 6d7e07a..4b1bcf8 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -7,23 +7,33 @@
 const { blue, bold, green, red } = require('chalk');
 const path = require('path');
 const semver = require('semver');
 const { exec } = require('child_process');
 const { promisify } = require('util');
+const validatePackageName = require('validate-npm-package-name');
 
 const execAsync = promisify(exec);
 
+function isValidNpmPackageName(name) {
+  const { errors } = validatePackageName(name);
+  if (errors) {
+    throw new Error(`NPM package name: "${name}" is invalid. ${errors}`);
+  }
+}
+
 async function getLatestVersions(name) {
+  isValidNpmPackageName(name);
   const { stdout } = await execAsync(`npm view ${name} versions --json`);
   try {
     return JSON.parse(stdout);
   } catch (err) {
     throw new Error(`Failed to parse output from NPM view - ${err.toString()}`);
   }
 }
 
 async function getLatestTag(name) {
+  isValidNpmPackageName(name);
   try {
     const { stdout } = await execAsync(`npm view ${name} dist-tags --json`);
     const { latest } = JSON.parse(stdout);
     return latest;
   } catch (err) {
diff --git a/lib/index.test.js b/lib/index.test.js
index 13acd96..0d2f7b0 100644
--- a/lib/index.test.js
+++ b/lib/index.test.js
@@ -343,8 +343,16 @@ describe('lib/index', () => {
       `npm i ${outdatedDep}@1.0.1 \nnpm i -D ${outdatedDevDep}@1.0.1 `
     );
     expect(logger.info).toHaveBeenNthCalledWith(6, `Upgraded dependencies:\n["1.0.0"]`);
     expect(logger.info).toHaveBeenNthCalledWith(7, `Upgraded development dependencies:\n["1.0.0"]`);
   });
+
+  test('throw error when npm module name is invalid', async () => {
+    mockExports.dependencies = { 'bad name Dependency': '1.2.3' };
+    mockExecAsync.mockImplementationOnce(mock);
+    await expect(verifyDeps({ autoUpgrade: true, dir, logger })).rejects.toThrow(
+      'NPM package name: "bad name Dependency" is invalid. name can only contain URL-friendly characters'
+    );
+  });
 });
 
 module.exports = mockExports;
diff --git a/package-lock.json b/package-lock.json
index 3179c35..bbcecfe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1563,10 +1563,15 @@
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
       "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
       "dev": true
     },
+    "builtins": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz",
+      "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og="
+    },
     "cache-base": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
       "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
       "dev": true,
@@ -8777,10 +8782,18 @@
       "requires": {
         "spdx-correct": "^3.0.0",
         "spdx-expression-parse": "^3.0.0"
       }
     },
+    "validate-npm-package-name": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz",
+      "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=",
+      "requires": {
+        "builtins": "^1.0.3"
+      }
+    },
     "verror": {
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
       "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
       "dev": true,
diff --git a/package.json b/package.json
index 6707a4d..877de8e 100644
--- a/package.json
+++ b/package.json
@@ -46,11 +46,12 @@
     "version": "auto-changelog -p && git add CHANGELOG.md"
   },
   "dependencies": {
     "chalk": "^3.0.0",
     "minimist": "^1.2.5",
-    "semver": "^7.3.4"
+    "semver": "^7.3.4",
+    "validate-npm-package-name": "^3.0.0"
   },
   "devDependencies": {
     "auto-changelog": "^1.16.4",
     "chance": "^1.1.7",
     "check-engines": "^1.5.0",

Vulnerable code:     "semver": "^7.3.4"
Fixed code: const validatePackageName = require('validate-npm-package-name');
function isValidNpmPackageName(name) {
  const { errors } = validatePackageName(name);
  if (errors) {
    throw new Error(`NPM package name: "${name}" is invalid. ${errors}`);
  }
}

  isValidNpmPackageName(name);
  isValidNpmPackageName(name);

  test('throw error when npm module name is invalid', async () => {
    mockExports.dependencies = { 'bad name Dependency': '1.2.3' };
    mockExecAsync.mockImplementationOnce(mock);
    await expect(verifyDeps({ autoUpgrade: true, dir, logger })).rejects.toThrow(
      'NPM package name: "bad name Dependency" is invalid. name can only contain URL-friendly characters'
    );
  });
    "builtins": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz",
      "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og="
    },
    "validate-npm-package-name": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz",
      "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=",
      "requires": {
        "builtins": "^1.0.3"
      }
    },
    "semver": "^7.3.4",
    "validate-npm-package-name": "^3.0.0"