CVE-2021-29369: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') (CWE-78)
value: The gnuplot package prior to version 0.1.0 for Node.js allows code execution via shell metacharacters in Gnuplot commands.
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: 9.8
Published: 2021-05-03T12:15Z
Repository: https://github.com/rkesters/gnuplot
Language: JSON
Files changed: package-lock.json, package.json, src/index.ts, src/test.ts
Commit date: 2021-01-22 15:14:05 -0500
Commit message: validate filename

Diff: diff --git a/package-lock.json b/package-lock.json
index 089843a..7d19314 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -404,12 +404,11 @@
       }
     },
     "escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
-      "dev": true
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
     },
     "esprima": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
       "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
@@ -419,10 +418,25 @@
       "version": "1.1.6",
       "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz",
       "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=",
       "dev": true
     },
+    "filename-reserved-regex": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz",
+      "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik="
+    },
+    "filenamify": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.2.0.tgz",
+      "integrity": "sha512-pkgE+4p7N1n7QieOopmn3TqJaefjdWXwEkj2XLZJLKfOgcQKkn11ahvGNgTD8mLggexLiDFQxeTs14xVU22XPA==",
+      "requires": {
+        "filename-reserved-regex": "^2.0.0",
+        "strip-outer": "^1.0.1",
+        "trim-repeated": "^1.0.0"
+      }
+    },
     "fill-range": {
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
       "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
       "dev": true,
@@ -984,10 +998,18 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
       "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
       "dev": true
     },
+    "strip-outer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
+      "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==",
+      "requires": {
+        "escape-string-regexp": "^1.0.2"
+      }
+    },
     "supports-color": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
       "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
       "dev": true,
@@ -1002,10 +1024,18 @@
       "dev": true,
       "requires": {
         "is-number": "^7.0.0"
       }
     },
+    "trim-repeated": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
+      "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=",
+      "requires": {
+        "escape-string-regexp": "^1.0.2"
+      }
+    },
     "ts-node": {
       "version": "8.10.2",
       "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz",
       "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==",
       "dev": true,
diff --git a/package.json b/package.json
index 8bc1505..d06ef14 100644
--- a/package.json
+++ b/package.json
@@ -52,8 +52,9 @@
     "tslint": "^5.11.0",
     "tslint-config-standard": "^8.0.1",
     "typescript": "^4.0.5"
   },
   "dependencies": {
+    "filenamify": "^4.2.0",
     "lodash": "^4.17.15"
   }
 }
diff --git a/src/index.ts b/src/index.ts
index 00c1d41..c69a267 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -352,15 +352,21 @@ function plotCallack(options: PlotOptions) {
   }
 
   gnuplot.stdin.end();
 }
 
+import validFilename from "filenamify";
+
 /**
  * Plots data to a PDF file. If it does not exist, the PDF file will
  * be created, otherwise this plot will be appended as a new page.
  */
 export function plot(options: PlotOptions): Promise<boolean> | void {
+  const f = validFilename.path(options.filename);
+  if (f !== options.filename) {
+    throw new Error(`invalid filename of '${options.filename}' -- ${f}`);
+  }
   if (options.finish) {
     return plotCallack(options);
   }
 
   // Promise
diff --git a/src/test.ts b/src/test.ts
index d72510e..0966ca3 100644
--- a/src/test.ts
+++ b/src/test.ts
@@ -4,10 +4,11 @@
  */
 import "mocha";
 
 import { plot } from "./index";
 import { expect as should } from "chai";
+import { fail } from "assert";
 
 function handleResult(
   error: any,
   stdout: any,
   stderr: { should: { be: { empty: () => void } } },
@@ -53,10 +54,26 @@ describe("Plot tests", function () {
   // describe('PNG output', function () {
 
   // });
 
   describe("PNG output", function () {
+    it("bad filename ", async () => {
+      let passed = false;
+      try {
+        const ploted = await plot({
+          data: [3, 1, 2, 3, 4],
+          filename: __dirname + "/test/output1.png & frog > frog.txt",
+          format: "png",
+        });
+      } catch (e) {
+        should(e.message).contain("invalid filename of");
+        passed = true;
+      }
+      if (!passed) {
+        fail();
+      }
+    });
     it("Async Output1", async () => {
       const ploted = await plot({
         data: [3, 1, 2, 3, 4],
         filename: __dirname + "/test/output1.png",
         format: "png",

Vulnerable code:       "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
      "dev": true
Fixed code:       "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
    "filename-reserved-regex": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz",
      "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik="
    },
    "filenamify": {
      "version": "4.2.0",
      "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.2.0.tgz",
      "integrity": "sha512-pkgE+4p7N1n7QieOopmn3TqJaefjdWXwEkj2XLZJLKfOgcQKkn11ahvGNgTD8mLggexLiDFQxeTs14xVU22XPA==",
      "requires": {
        "filename-reserved-regex": "^2.0.0",
        "strip-outer": "^1.0.1",
        "trim-repeated": "^1.0.0"
      }
    },
    "strip-outer": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
      "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==",
      "requires": {
        "escape-string-regexp": "^1.0.2"
      }
    },
    "trim-repeated": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
      "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=",
      "requires": {
        "escape-string-regexp": "^1.0.2"
      }
    },
    "filenamify": "^4.2.0",
import validFilename from "filenamify";

  const f = validFilename.path(options.filename);
  if (f !== options.filename) {
    throw new Error(`invalid filename of '${options.filename}' -- ${f}`);
  }
import { fail } from "assert";
    it("bad filename ", async () => {
      let passed = false;
      try {
        const ploted = await plot({
          data: [3, 1, 2, 3, 4],
          filename: __dirname + "/test/output1.png & frog > frog.txt",
          format: "png",
        });
      } catch (e) {
        should(e.message).contain("invalid filename of");
        passed = true;
      }
      if (!passed) {
        fail();
      }
    });