CVE-2020-11079: Improper Neutralization of Special Elements used in a Command ('Command Injection') (CWE-77)
value: node-dns-sync (npm module dns-sync) through 0.2.0 allows execution of arbitrary commands . This issue may lead to remote code execution if a client of the library calls the vulnerable method with untrusted input. This has been fixed in 0.2.1.
CWE 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.
CVSS v3 base score: 9.8
Published: 2020-05-28T19:15Z
Repository: https://github.com/skoranga/node-dns-sync
Language: JavaScript
Files changed: .eslintrc, .gitignore, .travis.yml, CHANGELOG.md, LICENSE, README.md, benchmarks/README.md, benchmarks/package.json, benchmarks/perf.js, index.js, lib/dns-sync.js, package.json, scripts/dns-lookup-script.js, test/test.js
Commit date: 2020-05-19 18:48:24 -0700
Commit message: allowing only valid rrtypes

Diff: diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..63c2890
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,14 @@
+{
+    "extends": [
+      "eslint:recommended"
+    ],
+    "env": {
+      "node": true,
+      "browser": false,
+      "es6": false,
+      "mocha": true
+    },
+    "rules": {
+        "no-useless-escape": 0
+    }
+  }
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a72b52e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+lib-cov
+*.seed
+*.log
+*.csv
+*.dat
+*.out
+*.pid
+*.gz
+
+pids
+logs
+results
+
+npm-debug.log
+node_modules
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..4f61427
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,8 @@
+language: node_js
+
+node_js:
+  - "6"
+  - "8"
+  - "10"
+  - "12"
+  - "14"
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..e24bef3
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,18 @@
+CHANGELOG
+==========
+
+0.2.1
+-----
+
+- Allowing only valid rrtypes as per https://nodejs.org/api/dns.html#dns_dns_resolve_hostname_rrtype_callback.
+
+0.2.0
+-----
+
+- Updating dependencies
+
+0.1.3
+-----
+
+- Added support for resolving AAAA and NS records
+- Upgrading to latest shelljs dependency
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5dfeaa9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 skoranga
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ac0f933
--- /dev/null
+++ b/README.md
@@ -0,0 +1,20 @@
+node-dns-sync
+=============
+
+[![Build Status](https://travis-ci.org/skoranga/node-dns-sync.png)](https://travis-ci.org/skoranga/node-dns-sync)
+
+Sync/Blocking DNS resolve. Main usecase is in node server startup.
+
+How to Use
+-------
+
+```javascript
+var dnsSync = require('dns-sync');
+
+console.log(dnsSync.resolve('www.paypal.com'));     //should return the IP address
+console.log(dnsSync.resolve('www.yahoo.com'));
+console.log(dnsSync.resolve('www.non-host.something')); //should return null
+
+console.log(dnsSync.resolve('www.google.com', 'AAAA')); //should return AAAA records
+console.log(dnsSync.resolve('google.com', 'NS'));   //should return NS record
+```
diff --git a/benchmarks/README.md b/benchmarks/README.md
new file mode 100644
index 0000000..b8d80bb
--- /dev/null
+++ b/benchmarks/README.md
@@ -0,0 +1,28 @@
+### how to
+
+cd benchmarks
+npm install
+node perf.js
+
+
+### Perf report
+
+***Node@v0.12.0***
+
+ **With shelljs - ~0.3.0**
+
+```
+ success case x 8.20 ops/sec ±1.32% (25 runs sampled)
+ failure case x 8.21 ops/sec ±1.59% (25 runs sampled)
+
+ 34% CPU and ≈66MB memory footprint
+```
+
+**With shelljs - ~0.4.0**
+
+```
+ success case x 9.37 ops/sec ±1.42% (28 runs sampled)
+ failure case x 9.39 ops/sec ±1.42% (28 runs sampled)
+
+  2% CPU and ≈20MB memory footprint
+```
\ No newline at end of file
diff --git a/benchmarks/package.json b/benchmarks/package.json
new file mode 100644
index 0000000..e9d9d11
--- /dev/null
+++ b/benchmarks/package.json
@@ -0,0 +1,12 @@
+{
+  "name": "dns-sync-perf",
+  "description": "dns-sync",
+  "version": "0.0.1",
+  "devDependencies": {
+    "benchmark" : "^2.0.0",
+    "microtime": "^2.0.0"
+  },
+  "scripts": {
+    "test": "node perf.js"
+  }
+}
diff --git a/benchmarks/perf.js b/benchmarks/perf.js
new file mode 100644
index 0000000..4ebff96
--- /dev/null
+++ b/benchmarks/perf.js
@@ -0,0 +1,19 @@
+'use strict';
+
+var Benchmark = require('benchmark');
+
+var dnsSync = require('../index');
+
+
+var suite = new Benchmark.Suite();
+
+suite.add('success case', function() {
+    return dnsSync.resolve('www.example.com');
+})
+.add('failure case', function() {
+  return dnsSync.resolve('www.example.con');
+})
+.on('cycle', function(event) {
+  console.log(String(event.target));
+})
+.run({ 'async': true });
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..9f7968a
--- /dev/null
+++ b/index.js
@@ -0,0 +1,3 @@
+'use strict';
+
+module.exports = require('./lib/dns-sync');
diff --git a/lib/dns-sync.js b/lib/dns-sync.js
new file mode 100644
index 0000000..d0574cb
--- /dev/null
+++ b/lib/dns-sync.js
@@ -0,0 +1,59 @@
+'use strict';
+
+var util = require('util'),
+    path = require('path'),
+    shell = require('shelljs'),
+    debug = require('debug')('dns-sync');
+
+//source - http://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
+var ValidHostnameRegex = new RegExp("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$");
+
+// https://nodejs.org/api/dns.html#dns_dns_resolve_hostname_rrtype_callback
+var RRecordTypes = [
+    'A',
+    'AAAA',
+    'NS',
+    'NAPTR',
+    'CNAME',
+    'SOA',
+    'SRV',
+    'PTR',
+    'MX',
+    'TXT',
+    'ANY'];
+
+function isValidHostName(hostname) {
+    return ValidHostnameRegex.test(hostname);
+}
+/**
+ * Resolve hostname to IP address,
+ * returns null in case of error
+ */
+module.exports = {
+    lookup: function lookup(hostname) {
+        return module.exports.resolve(hostname);
+    },
+    resolve: function resolve(hostname, type) {
+        var nodeBinary = process.execPath;
+
+        if (!isValidHostName(hostname)) {
+            console.error('Invalid hostname:', hostname);
+            return null;
+        }
+        if (typeof type !== 'undefined' && RRecordTypes.indexOf(type) === -1) {
+            console.error('Invalid rrtype:', type);
+            return null;
+        }
+
+        var scriptPath = path.join(__dirname, "../scripts/dns-lookup-script"),
+            response,
+            cmd = util.format('"%s" "%s" %s %s', nodeBinary, scriptPath, hostname, type || '');
+
+        response = shell.exec(cmd, {silent: true});
+        if (response && response.code === 0) {
+            return JSON.parse(response.stdout);
+        }
+        debug('hostname', "fail to resolve hostname " + hostname);
+        return null;
+    }
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..8c82466
--- /dev/null
+++ b/package.json
@@ -0,0 +1,32 @@
+{
+  "name": "dns-sync",
+  "version": "0.2.1",
+  "description": "dns-sync",
+  "main": "index.js",
+  "scripts": {
+    "test": "mocha",
+    "lint": "eslint ."
+  },
+  "homepage": "https://github.com/skoranga/node-dns-sync",
+  "repository": {
+    "type": "git",
+    "url": "git@github.com:skoranga/node-dns-sync.git"
+  },
+  "keywords": [
+    "dns sync",
+    "server startup",
+    "nodejs"
+  ],
+  "author": "Sanjeev Koranga",
+  "license": "MIT",
+  "readmeFilename": "README.md",
+  "dependencies": {
+    "debug": "^4",
+    "shelljs": "~0.8"
+  },
+  "devDependencies": {
+    "mocha": "^6",
+    "eslint": "^6",
+    "eslint-plugin-standard": "^4"
+  }
+}
diff --git a/scripts/dns-lookup-script.js b/scripts/dns-lookup-script.js
new file mode 100644
index 0000000..fea92b6
--- /dev/null
+++ b/scripts/dns-lookup-script.js
@@ -0,0 +1,24 @@
+'use strict';
+
+var dns = require('dns'),
+    debug = require('debug')('dns-sync'),
+    name, type, fn;
+
+for (var i = 0; i < process.argv.length; i++) {
+    if (process.argv[i].indexOf('dns-lookup-script') >= 0) {
+        name = process.argv[i + 1];
+        type = process.argv[i + 2];
+        fn = type ? dns.resolve.bind(dns, name, type) : dns.lookup.bind(dns, name);
+        break;
+    }
+}
+
+fn(function (err, ip) {
+    if (err) {
+        debug(err);
+        process.exit(1);
+    } else {
+        debug(name, 'resolved to', ip);
+        process.stdout.write(JSON.stringify(ip));
+    }
+});
diff --git a/test/test.js b/test/test.js
new file mode 100644
index 0000000..712e090
--- /dev/null
+++ b/test/test.js
@@ -0,0 +1,50 @@
+'use strict';
+
+var assert = require('assert'),
+    fs = require('fs'),
+    dnsSync = require('../index');
+
+describe('dns sync', function () {
+
+    it('should resolve dns', function () {
+        assert.ok(dnsSync.resolve('www.example.com'));
+        assert.ok(dnsSync.resolve('www.paypal.com'));
+        assert.ok(dnsSync.resolve('www.google.com'));
+        assert.ok(dnsSync.resolve('www.yahoo.com'));
+    });
+
+    it('should fail to resolve dns', function () {
+        assert.ok(!dnsSync.resolve('www.example.con'));
+        assert.ok(!dnsSync.resolve('www.paypal.con'));
+        assert.ok(!dnsSync.resolve('www.not-google.first'));
+        assert.ok(!dnsSync.resolve('www.hello-yahoo.next'));
+    });
+
+    it('should fail to resolve invalid dns', function () {
+        assert.ok(!dnsSync.resolve("$(id > /tmp/foo)'"));
+        assert.ok(!dnsSync.resolve("cd /tmp; rm -f /tmp/echo; env 'x=() { (a)=>' bash -c \"echo date\"; cat /tmp/echo"));
+        assert.ok(!dnsSync.resolve("$(grep -l -z '[^)]=() {' /proc/[1-9]*/environ | cut -d/ -f3)'"));
+    });
+
+    it('should resolve AAAA records if asked', function () {
+        var aaaa = dnsSync.resolve('www.google.com', 'AAAA');
+        assert.ok(aaaa);
+        assert.ok(aaaa[0].match(/^([0-9a-f]{2,4}(:|$))+/));
+        assert.ok(dnsSync.resolve('www.google.com') !== aaaa);
+    });
+
+    it('should resolve NS records if asked', function () {
+        var ns = dnsSync.resolve('google.com', 'NS');
+        assert.ok(ns);
+        assert.ok(ns.length >= 1);
+        assert.ok(ns[0].match(/^ns[0-9]+\.google\.com$/));
+    });
+
+    it('should fail to resolve if invalid record is asked', function () {
+        var rs1 = dnsSync.resolve('www.google.com', 'Test');
+        var rs2 = dnsSync.resolve('www.google.com', ' && touch test.txt');
+        assert.ok(!rs1);
+        assert.ok(!rs2);
+        assert.ok(!fs.existsSync('test.txt'));
+    });
+});

Fixed code: {
    "extends": [
      "eslint:recommended"
    ],
    "env": {
      "node": true,
      "browser": false,
      "es6": false,
      "mocha": true
    },
    "rules": {
        "no-useless-escape": 0
    }
  }
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz

pids
logs
results

npm-debug.log
node_modules
language: node_js

node_js:
  - "6"
  - "8"
  - "10"
  - "12"
  - "14"
CHANGELOG
==========

0.2.1
-----

- Allowing only valid rrtypes as per https://nodejs.org/api/dns.html#dns_dns_resolve_hostname_rrtype_callback.

0.2.0
-----

- Updating dependencies

0.1.3
-----

- Added support for resolving AAAA and NS records
- Upgrading to latest shelljs dependency
The MIT License (MIT)

Copyright (c) 2014 skoranga

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
node-dns-sync
=============

[![Build Status](https://travis-ci.org/skoranga/node-dns-sync.png)](https://travis-ci.org/skoranga/node-dns-sync)

Sync/Blocking DNS resolve. Main usecase is in node server startup.

How to Use
-------

```javascript
var dnsSync = require('dns-sync');

console.log(dnsSync.resolve('www.paypal.com'));     //should return the IP address
console.log(dnsSync.resolve('www.yahoo.com'));
console.log(dnsSync.resolve('www.non-host.something')); //should return null

console.log(dnsSync.resolve('www.google.com', 'AAAA')); //should return AAAA records
console.log(dnsSync.resolve('google.com', 'NS'));   //should return NS record
```
### how to

cd benchmarks
npm install
node perf.js


### Perf report

***Node@v0.12.0***

 **With shelljs - ~0.3.0**

```
 success case x 8.20 ops/sec ±1.32% (25 runs sampled)
 failure case x 8.21 ops/sec ±1.59% (25 runs sampled)

 34% CPU and ≈66MB memory footprint
```

**With shelljs - ~0.4.0**

```
 success case x 9.37 ops/sec ±1.42% (28 runs sampled)
 failure case x 9.39 ops/sec ±1.42% (28 runs sampled)

  2% CPU and ≈20MB memory footprint
```
{
  "name": "dns-sync-perf",
  "description": "dns-sync",
  "version": "0.0.1",
  "devDependencies": {
    "benchmark" : "^2.0.0",
    "microtime": "^2.0.0"
  },
  "scripts": {
    "test": "node perf.js"
  }
}
'use strict';

var Benchmark = require('benchmark');

var dnsSync = require('../index');


var suite = new Benchmark.Suite();

suite.add('success case', function() {
    return dnsSync.resolve('www.example.com');
})
.add('failure case', function() {
  return dnsSync.resolve('www.example.con');
})
.on('cycle', function(event) {
  console.log(String(event.target));
})
.run({ 'async': true });
'use strict';

module.exports = require('./lib/dns-sync');
'use strict';

var util = require('util'),
    path = require('path'),
    shell = require('shelljs'),
    debug = require('debug')('dns-sync');

//source - http://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
var ValidHostnameRegex = new RegExp("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$");

// https://nodejs.org/api/dns.html#dns_dns_resolve_hostname_rrtype_callback
var RRecordTypes = [
    'A',
    'AAAA',
    'NS',
    'NAPTR',
    'CNAME',
    'SOA',
    'SRV',
    'PTR',
    'MX',
    'TXT',
    'ANY'];

function isValidHostName(hostname) {
    return ValidHostnameRegex.test(hostname);
}
/**
 * Resolve hostname to IP address,
 * returns null in case of error
 */
module.exports = {
    lookup: function lookup(hostname) {
        return module.exports.resolve(hostname);
    },
    resolve: function resolve(hostname, type) {
        var nodeBinary = process.execPath;

        if (!isValidHostName(hostname)) {
            console.error('Invalid hostname:', hostname);
            return null;
        }
        if (typeof type !== 'undefined' && RRecordTypes.indexOf(type) === -1) {
            console.error('Invalid rrtype:', type);
            return null;
        }

        var scriptPath = path.join(__dirname, "../scripts/dns-lookup-script"),
            response,
            cmd = util.format('"%s" "%s" %s %s', nodeBinary, scriptPath, hostname, type || '');

        response = shell.exec(cmd, {silent: true});
        if (response && response.code === 0) {
            return JSON.parse(response.stdout);
        }
        debug('hostname', "fail to resolve hostname " + hostname);
        return null;
    }
};
{
  "name": "dns-sync",
  "version": "0.2.1",
  "description": "dns-sync",
  "main": "index.js",
  "scripts": {
    "test": "mocha",
    "lint": "eslint ."
  },
  "homepage": "https://github.com/skoranga/node-dns-sync",
  "repository": {
    "type": "git",
    "url": "git@github.com:skoranga/node-dns-sync.git"
  },
  "keywords": [
    "dns sync",
    "server startup",
    "nodejs"
  ],
  "author": "Sanjeev Koranga",
  "license": "MIT",
  "readmeFilename": "README.md",
  "dependencies": {
    "debug": "^4",
    "shelljs": "~0.8"
  },
  "devDependencies": {
    "mocha": "^6",
    "eslint": "^6",
    "eslint-plugin-standard": "^4"
  }
}
'use strict';

var dns = require('dns'),
    debug = require('debug')('dns-sync'),
    name, type, fn;

for (var i = 0; i < process.argv.length; i++) {
    if (process.argv[i].indexOf('dns-lookup-script') >= 0) {
        name = process.argv[i + 1];
        type = process.argv[i + 2];
        fn = type ? dns.resolve.bind(dns, name, type) : dns.lookup.bind(dns, name);
        break;
    }
}

fn(function (err, ip) {
    if (err) {
        debug(err);
        process.exit(1);
    } else {
        debug(name, 'resolved to', ip);
        process.stdout.write(JSON.stringify(ip));
    }
});
'use strict';

var assert = require('assert'),
    fs = require('fs'),
    dnsSync = require('../index');

describe('dns sync', function () {

    it('should resolve dns', function () {
        assert.ok(dnsSync.resolve('www.example.com'));
        assert.ok(dnsSync.resolve('www.paypal.com'));
        assert.ok(dnsSync.resolve('www.google.com'));
        assert.ok(dnsSync.resolve('www.yahoo.com'));
    });

    it('should fail to resolve dns', function () {
        assert.ok(!dnsSync.resolve('www.example.con'));
        assert.ok(!dnsSync.resolve('www.paypal.con'));
        assert.ok(!dnsSync.resolve('www.not-google.first'));
        assert.ok(!dnsSync.resolve('www.hello-yahoo.next'));
    });

    it('should fail to resolve invalid dns', function () {
        assert.ok(!dnsSync.resolve("$(id > /tmp/foo)'"));
        assert.ok(!dnsSync.resolve("cd /tmp; rm -f /tmp/echo; env 'x=() { (a)=>' bash -c \"echo date\"; cat /tmp/echo"));
        assert.ok(!dnsSync.resolve("$(grep -l -z '[^)]=() {' /proc/[1-9]*/environ | cut -d/ -f3)'"));
    });

    it('should resolve AAAA records if asked', function () {
        var aaaa = dnsSync.resolve('www.google.com', 'AAAA');
        assert.ok(aaaa);
        assert.ok(aaaa[0].match(/^([0-9a-f]{2,4}(:|$))+/));
        assert.ok(dnsSync.resolve('www.google.com') !== aaaa);
    });

    it('should resolve NS records if asked', function () {
        var ns = dnsSync.resolve('google.com', 'NS');
        assert.ok(ns);
        assert.ok(ns.length >= 1);
        assert.ok(ns[0].match(/^ns[0-9]+\.google\.com$/));
    });

    it('should fail to resolve if invalid record is asked', function () {
        var rs1 = dnsSync.resolve('www.google.com', 'Test');
        var rs2 = dnsSync.resolve('www.google.com', ' && touch test.txt');
        assert.ok(!rs1);
        assert.ok(!rs2);
        assert.ok(!fs.existsSync('test.txt'));
    });
});