CVE-2020-4059 (published 2020-06-18T20:15Z)

In mversion before 2.0.0, there is a command injection vulnerability. This issue may lead to remote code execution if a client of the library calls the vulnerable method with untrusted input. This vulnerability is patched by version 2.0.0. Previous releases are deprecated in npm. As a workaround, make sure to escape git commit messages when using the commitMessage option for the update function.

Severity: HIGH
CVSS v2 base score: 7.5 (AV:N/AC:L/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 7.3, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L)

Weakness information:
cwe_id: CWE-77
cwe_name: Improper Neutralization of Special Elements used in a Command ('Command Injection')
description: The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.
extended_description: 
            Many protocols and products have their own custom command language. While OS or shell command strings are frequently discovered and targeted, developers may not realize that these other command languages might also be vulnerable to attacks.
         
         

Fix information:
repo_url: https://github.com/mikaelbr/mversion
commit_details: repo_url: https://github.com/mikaelbr/mversion
author: Mikael Brevik
author_date: 2020-06-15 18:14:37+02:00
msg: Fixes missing shell escape for git commit message
repository: repo_url: https://github.com/mikaelbr/mversion
repo_name: mikaelbr/mversion
description: A cross packaging module version bumper. CLI or API for bumping versions of package.json, bower.json, *.jquery.json etc.
repo_language: JavaScript
owner: mikaelbr
file_changes: filename: git.js
change_type: ModificationType.MODIFY
diff: @@ -1,62 +1,71 @@
-var contra = require('contra'),
-    path = require('path'),
-    fUtils = require('./files'),
-    cp = require('child_process');
-
-var gitApp = 'git', gitExtra = { env: process.env };
+var contra = require("contra"),
+  path = require("path"),
+  fUtils = require("./files"),
+  cp = require("child_process");
 
+var gitApp = "git",
+  gitExtra = { env: process.env };
 
 var escapeQuotes = function (str) {
-  if (typeof str === 'string') {
-    return str.replace(/(["$`\\])/g, '\\$1');
+  if (typeof str === "string") {
+    return '"' + str.replace(/(["'$`\\])/g, "\\$1") + '"';
   } else {
     return str;
   }
 };
 
 module.exports.isRepositoryClean = function (callback) {
-  cp.exec(gitApp + ' ' + [ 'ls-files', '-m' ].join(' '), gitExtra, function (er, stdout, stderr) {
+  cp.exec(gitApp + " " + ["ls-files", "-m"].join(" "), gitExtra, function (
+    er,
+    stdout,
+    stderr
+  ) {
     // makeCommit parly inspired and taken from NPM version module
-    var lines = stdout.trim().split('\n').filter(function (line) {
-      var file = path.basename(line.replace(/.{1,2}\s+/, ''));
-      return line.trim() && !line.match(/^\?\? /) && !fUtils.isPackageFile(line);
-    }).map(function (line) {
-      return line.trim()
-    });
+    var lines = stdout
+      .trim()
+      .split("\n")
+      .filter(function (line) {
+        var file = path.basename(line.replace(/.{1,2}\s+/, ""));
+        return (
+          line.trim() && !line.match(/^\?\? /) && !fUtils.isPackageFile(line)
+        );
+      })
+      .map(function (line) {
+        return line.trim();
+      });
 
     if (lines.length) {
-      return callback(new Error('Git working directory not clean.\n'+lines.join('\n')));
+      return callback(
+        new Error("Git working directory not clean.\n" + lines.join("\n"))
+      );
     }
     return callback();
   });
 };
 
 module.exports.checkout = function (callback) {
-  cp.exec(gitApp + ' checkout -- .', gitExtra, callback);
+  cp.exec(gitApp + " checkout -- .", gitExtra, callback);
 };
 
 module.exports.commit = function (files, message, newVer, tagName, callback) {
-  message = message.replace('%s', newVer).replace('"', '').replace("'", '');
-  files = files.map(function (file) {
-    return '"' + escapeQuotes(file) + '"';
-  }).join(' ');
+  message = escapeQuotes(message.replace("%s", newVer));
+  files = files.map(escapeQuotes).join(" ");
   var functionSeries = [
     function (done) {
-      cp.exec(gitApp + ' add ' + files, gitExtra, done);
+      cp.exec(gitApp + " add " + files, gitExtra, done);
     },
 
     function (done) {
-      cp.exec([gitApp, 'commit', '-m', '"' + message + '"'].join(' '), gitExtra, done);
+      cp.exec([gitApp, "commit", "-m", message].join(" "), gitExtra, done);
     },
 
     function (done) {
       cp.exec(
-        [
-          gitApp, 'tag', '-a', tagName, '-m', '"' + message + '"'
-        ].join(' '),
-        gitExtra, done
+        [gitApp, "tag", "-a", tagName, "-m", message].join(" "),
+        gitExtra,
+        done
       );
-    }
+    },
   ];
   contra.series(functionSeries, callback);
 };

programming_language: JavaScript
filename: git_test.js
change_type: ModificationType.MODIFY
diff: @@ -1,17 +1,17 @@
-var version = require('../'),
-    assert = require('assert'),
-    fs = require('fs'),
-    vinylFs = require('vinyl-fs'),
-    path = require('path'),
-    cp = require('child_process'),
-    File = require('vinyl'),
-    through = require('through2'),
-    fUtil = require('../lib/files'),
-    git = require('../lib/git');
-
-describe('git', function () {
-  var filename = 'package.json';
-  var expectedPath = path.join(__dirname, './fixtures/', filename);
+var version = require("../"),
+  assert = require("assert"),
+  fs = require("fs"),
+  vinylFs = require("vinyl-fs"),
+  path = require("path"),
+  cp = require("child_process"),
+  File = require("vinyl"),
+  through = require("through2"),
+  fUtil = require("../lib/files"),
+  git = require("../lib/git");
+
+describe("git", function () {
+  var filename = "package.json";
+  var expectedPath = path.join(__dirname, "./fixtures/", filename);
   var expectedContent = fs.readFileSync(expectedPath);
 
   var original = fUtil.loadFiles;
@@ -21,19 +21,19 @@ describe('git', function () {
   var originalCommit = git.commit;
   var originalCheckout = git.checkout;
 
-  before(function () {
+  before(function () {
     vinylFs.dest = function () {
       return through.obj(function (file, enc, next) {
         this.push(file);
         next();
       });
-    }
+    };
 
     var expectedFile = new File({
       base: __dirname,
       cwd: __dirname,
       path: expectedPath,
-      contents: expectedContent
+      contents: expectedContent,
     });
 
     fUtil.loadFiles = function () {
@@ -57,130 +57,162 @@ describe('git', function () {
     cp.exec = exec;
   });
 
-  describe('#Update()', function(){
-    it('should return error on unclean git repository when commit is given', function (done) {
+  describe("#Update()", function () {
+    it("should return error on unclean git repository when commit is given", function (done) {
       git.isRepositoryClean = function (cb) {
-        return cb(new Error('Not clean'));
+        return cb(new Error("Not clean"));
       };
 
-      version.update({
-        version: '1.0.0',
-        commitMessage: 'Message'
-      }, function (err, data) {
-        assert.ok(err);
-        assert.equal(err.message, 'Not clean', 'Error message should be set by isRepositoryClean');
+      version.update(
+        {
+          version: "1.0.0",
+          commitMessage: "Message",
+        },
+        function (err, data) {
+          assert.ok(err);
+          assert.equal(
+            err.message,
+            "Not clean",
+            "Error message should be set by isRepositoryClean"
+          );
+
+          done();
+        }
+      );
+    });
 
+    it("should return NOT error on unclean git repository when no commit message is given", function (done) {
+      git.isRepositoryClean = function (cb) {
+        return cb(new Error("Not clean"));
+      };
+
+      version.update("1.0.0", function (err, data) {
+        assert.ifError(err);
         done();
       });
     });
 
-    it('should return NOT error on unclean git repository when no commit message is given', function (done) {
+    it("should sanitize commit message", function (done) {
       git.isRepositoryClean = function (cb) {
-        return cb(new Error('Not clean'));
+        return cb(null);
       };
 
-      version.update('1.0.0', function (err, data) {
-        assert.ifError(err);
+      cp.exec = function (cmd, extra, cb) {
+        if (cmd.indexOf("-a") === -1) return cb(null);
+        assert.equal('git tag -a v1.0.0 -m "Message \\`touch file\\`"', cmd);
         done();
+      };
+
+      version.update({
+        version: "1.0.0",
+        commitMessage: "Message `touch file`",
       });
     });
 
-    it('should get updated version sent to commit when commit message is given', function (done) {
+    it("should get updated version sent to commit when commit message is given", function (done) {
       git.isRepositoryClean = function (cb) {
         return cb(null);
       };
 
       git.commit = function (files, message, newVer, tagName, callback) {
-        assert.equal(message, 'Message');
-        assert.equal(newVer, '1.0.0');
+        assert.equal(message, "Message");
+        assert.equal(newVer, "1.0.0");
         assert.equal(files[0], expectedPath);
-        assert.equal(tagName, 'v1.0.0');
+        assert.equal(tagName, "v1.0.0");
         return callback(null);
       };
 
-      version.update({
-        version: '1.0.0',
-        commitMessage: 'Message'
-      }, function (err, data) {
-        assert.ifError(err);
-        done();
-      });
+      version.update(
+        {
+          version: "1.0.0",
+          commitMessage: "Message",
+        },
+        function (err, data) {
+          assert.ifError(err);
+          done();
+        }
+      );
     });
 
-    it('should be able to override tagName', function (done) {
+    it("should be able to override tagName", function (done) {
       git.isRepositoryClean = function (cb) {
         return cb(null);
       };
 
       git.commit = function (files, message, newVer, tagName, callback) {
-        assert.equal(tagName, 'v1.0.0-src');
+        assert.equal(tagName, "v1.0.0-src");
         return callback(null);
       };
 
-      version.update({
-        version: '1.0.0',
-        commitMessage: 'Message',
-        tagName: 'v%s-src'
-      }, function (err, data) {
-        assert.ifError(err);
-        done();
-      });
+      version.update(
+        {
+          version: "1.0.0",
+          commitMessage: "Message",
+          tagName: "v%s-src",
+        },
+        function (err, data) {
+          assert.ifError(err);
+          done();
+        }
+      );
     });
 
-    it('should get flag defining if v-prefix should be used or not', function (done) {
+    it("should get flag defining if v-prefix should be used or not", function (done) {
       git.isRepositoryClean = function (cb) {
         return cb(null);
       };
 
       git.commit = function (files, message, newVer, noPrefix, callback) {
-        assert.ok(noPrefix, 'No prefix should be true');
+        assert.ok(noPrefix, "No prefix should be true");
         return callback(null);
       };
 
-      version.update({
-        version: '1.0.0',
-        commitMessage: 'Message',
-        noPrefix: true
-      }, function (err, data) {
-        assert.ifError(err);
-        done();
-      });
+      version.update(
+        {
+          version: "1.0.0",
+          commitMessage: "Message",
+          noPrefix: true,
+        },
+        function (err, data) {
+          assert.ifError(err);
+          done();
+        }
+      );
     });
 
-    it('should make tag with v-prefix per default', function (done) {
+    it("should make tag with v-prefix per default", function (done) {
       git.isRepositoryClean = function (cb) {
         return cb(null);
       };
 
       cp.exec = function (cmd, extra, cb) {
-        if (cmd.indexOf('-a') === -1) return cb(null);
+        if (cmd.indexOf("-a") === -1) return cb(null);
         assert.equal('git tag -a v1.0.0 -m "Message"', cmd);
         done();
       };
 
       version.update({
-        version: '1.0.0',
-        commitMessage: 'Message'
+        version: "1.0.0",
+        commitMessage: "Message",
       });
     });
 
-    it('should make tag without v-prefix if specified', function (done) {
+    it("should make tag without v-prefix if specified", function (done) {
       git.isRepositoryClean = function (cb) {
         return cb(null);
       };
 
       cp.exec = function (cmd, extra, cb) {
-        if (cmd.indexOf('-a') === -1) return cb(null);
+        if (cmd.indexOf("-a") === -1) return cb(null);
         assert.equal('git tag -a 1.0.0 -m "Message"', cmd);
         done();
       };
 
       version.update({
-        version: '1.0.0',
-        commitMessage: 'Message',
-        noPrefix: true
+        version: "1.0.0",
+        commitMessage: "Message",
+        noPrefix: true,
       });
     });
   });
-
-});
\ No newline at end of file
+});

programming_language: JavaScript