VYPR
High severityNVD Advisory· Published Jun 18, 2020· Updated Aug 4, 2024

Command Injection in mversion

CVE-2020-4059

Description

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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
mversionnpm
< 2.0.02.0.0

Affected products

1

Patches

1
6c76c9efd27c

Fixes missing shell escape for git commit message

https://github.com/mikaelbr/mversionMikael BrevikJun 15, 2020via ghsa
2 files changed · +141 100
  • lib/git.js+37 28 modified
    @@ -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);
     };
    
  • tests/git_test.js+104 72 modified
    @@ -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
    +});
    

Vulnerability mechanics

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

References

4

News mentions

0

No linked articles in our index yet.