From 73a7d406a543cfaa63b21d3b80d1cafba133f80a Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Tue, 18 Feb 2020 18:54:27 +0100
Subject: [PATCH 01/37] add query for useless use of cat
---
.../ql/src/Security/CWE-078/UselessCat.ql | 20 +++++
.../javascript/security/UselessUseOfCat.qll | 59 +++++++++++++++
.../CWE-078/CommandInjection.expected | 9 +++
.../Security/CWE-078/UselessCat.expected | 2 +
.../Security/CWE-078/UselessCat.ql | 18 +++++
.../Security/CWE-078/uselesscat.js | 73 +++++++++++++++++++
6 files changed, 181 insertions(+)
create mode 100644 javascript/ql/src/Security/CWE-078/UselessCat.ql
create mode 100644 javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
create mode 100644 javascript/ql/test/query-tests/Security/CWE-078/UselessCat.expected
create mode 100644 javascript/ql/test/query-tests/Security/CWE-078/UselessCat.ql
create mode 100644 javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
diff --git a/javascript/ql/src/Security/CWE-078/UselessCat.ql b/javascript/ql/src/Security/CWE-078/UselessCat.ql
new file mode 100644
index 000000000000..d2f0e13fe2e1
--- /dev/null
+++ b/javascript/ql/src/Security/CWE-078/UselessCat.ql
@@ -0,0 +1,20 @@
+/**
+ * @name Useless use of cat
+ * @description Using cat to simply read a file can lead to unintended bugs, and at worst security issues.
+ * @kind problem
+ * @problem.severity error
+ * @precision high
+ * @id js/useless-use-of-cat
+ * @tags correctness
+ * security
+ * external/cwe/cwe-078
+ * external/cwe/cwe-088
+ */
+
+import javascript
+import semmle.javascript.security.UselessUseOfCat
+
+from UselessCat cat
+select cat.getCommand(), "Useless use of `cat` in $@.", cat, "command execution"
+
+
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
new file mode 100644
index 000000000000..caf6c2a666ad
--- /dev/null
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -0,0 +1,59 @@
+/**
+ * Provides predicates and classes for working with useless uses of `cat`.
+ */
+
+import javascript
+import semmle.javascript.security.dataflow.IndirectCommandArgument
+
+/**
+ * Gets the first string from a string/string-concatenation.
+ */
+private string getStartingString(DataFlow::Node node) {
+ node.mayHaveStringValue(result) or
+ node.(StringOps::ConcatenationRoot).getFirstLeaf().mayHaveStringValue(result)
+}
+
+/**
+ * Gets a string from a string/string-concatenation.
+ */
+private string getAString(DataFlow::Node node) {
+ node.mayHaveStringValue(result) or
+ node.(StringOps::ConcatenationRoot).getALeaf().mayHaveStringValue(result)
+}
+
+/**
+ * An command-line execution of `cat` that only reads a file.
+ */
+class UselessCat extends DataFlow::Node {
+ DataFlow::Node command;
+
+ UselessCat() {
+ command = this.(SystemCommandExecution).getACommandArgument() and
+ exists(string cat |
+ cat = "cat" or cat = "/bin/cat" or cat = "sudo cat" or cat = "sudo /bin/cat"
+ |
+ exists(string commandString |
+ commandString = getStartingString(command).trim() and
+ (commandString = cat or commandString.regexpMatch(cat + " .*"))
+ ) and
+ // `cat` is OK in combination with pipes and wildcards.
+ not getAString(command).regexpMatch(".*(\\*|\\|).*") and
+ // It is OK just to spawn "cat" without any arguments.
+ not (
+ command.mayHaveStringValue(cat) and
+ not exists(
+ this
+ .(SystemCommandExecution)
+ .getArgumentList()
+ .(DataFlow::ArrayCreationNode)
+ .getAnElement()
+ )
+ )
+ )
+ }
+
+ /**
+ * Gets the dataflow node determining the command executed.
+ */
+ DataFlow::Node getCommand() { result = command }
+}
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection.expected b/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection.expected
index 589deda96c96..3211c1196453 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection.expected
@@ -89,6 +89,10 @@ nodes
| third-party-command-injection.js:5:20:5:26 | command |
| third-party-command-injection.js:6:21:6:27 | command |
| third-party-command-injection.js:6:21:6:27 | command |
+| uselesscat.js:18:20:18:54 | 'cat ' ... ms.data |
+| uselesscat.js:18:20:18:54 | 'cat ' ... ms.data |
+| uselesscat.js:18:40:18:54 | req.params.data |
+| uselesscat.js:18:40:18:54 | req.params.data |
edges
| child_process-test.js:6:9:6:49 | cmd | child_process-test.js:17:13:17:15 | cmd |
| child_process-test.js:6:9:6:49 | cmd | child_process-test.js:17:13:17:15 | cmd |
@@ -178,6 +182,10 @@ edges
| third-party-command-injection.js:5:20:5:26 | command | third-party-command-injection.js:6:21:6:27 | command |
| third-party-command-injection.js:5:20:5:26 | command | third-party-command-injection.js:6:21:6:27 | command |
| third-party-command-injection.js:5:20:5:26 | command | third-party-command-injection.js:6:21:6:27 | command |
+| uselesscat.js:18:40:18:54 | req.params.data | uselesscat.js:18:20:18:54 | 'cat ' ... ms.data |
+| uselesscat.js:18:40:18:54 | req.params.data | uselesscat.js:18:20:18:54 | 'cat ' ... ms.data |
+| uselesscat.js:18:40:18:54 | req.params.data | uselesscat.js:18:20:18:54 | 'cat ' ... ms.data |
+| uselesscat.js:18:40:18:54 | req.params.data | uselesscat.js:18:20:18:54 | 'cat ' ... ms.data |
#select
| child_process-test.js:17:13:17:15 | cmd | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:17:13:17:15 | cmd | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
| child_process-test.js:18:17:18:19 | cmd | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:18:17:18:19 | cmd | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
@@ -211,3 +219,4 @@ edges
| other.js:18:22:18:24 | cmd | other.js:5:25:5:31 | req.url | other.js:18:22:18:24 | cmd | This command depends on $@. | other.js:5:25:5:31 | req.url | a user-provided value |
| other.js:19:36:19:38 | cmd | other.js:5:25:5:31 | req.url | other.js:19:36:19:38 | cmd | This command depends on $@. | other.js:5:25:5:31 | req.url | a user-provided value |
| third-party-command-injection.js:6:21:6:27 | command | third-party-command-injection.js:5:20:5:26 | command | third-party-command-injection.js:6:21:6:27 | command | This command depends on $@. | third-party-command-injection.js:5:20:5:26 | command | a server-provided value |
+| uselesscat.js:18:20:18:54 | 'cat ' ... ms.data | uselesscat.js:18:40:18:54 | req.params.data | uselesscat.js:18:20:18:54 | 'cat ' ... ms.data | This command depends on $@. | uselesscat.js:18:40:18:54 | req.params.data | a user-provided value |
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.expected b/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.expected
new file mode 100644
index 000000000000..b19c48336703
--- /dev/null
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.expected
@@ -0,0 +1,2 @@
+| False negative | uselesscat.js:69:42:69:69 | // NOT ... lagged] |
+| False positive | uselesscat.js:18:70:18:118 | // OK [ ... jection |
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.ql b/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.ql
new file mode 100644
index 000000000000..bfb342f7ad15
--- /dev/null
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.ql
@@ -0,0 +1,18 @@
+import javascript
+import semmle.javascript.security.UselessUseOfCat
+
+from LineComment comment, string msg
+where
+ comment.getFile().getAbsolutePath().regexpMatch(".*/uselesscat.js") and
+ (
+ comment.getText().regexpMatch(".*NOT OK.*") and
+ not any(UselessCat cat).asExpr().getLocation().getStartLine() =
+ comment.getLocation().getStartLine() and
+ msg = "False negative"
+ or
+ comment.getText().regexpMatch(".* OK.*") and
+ not comment.getText().regexpMatch(".*NOT OK.*") and
+ any(UselessCat cat).asExpr().getLocation().getStartLine() = comment.getLocation().getStartLine() and
+ msg = "False positive"
+ )
+select msg, comment
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
new file mode 100644
index 000000000000..d613908e8d36
--- /dev/null
+++ b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
@@ -0,0 +1,73 @@
+var express = require('express');
+var child_process = require('child_process');
+var execSync = child_process.execSync;
+var exec = child_process.exec;
+var spawn = child_process.spawn;
+var spawnSync = child_process.spawnSync;
+var fs = require('fs');
+var app = express();
+
+function readStatus(id) {
+ exec("cat /proc/" + id + "/status", function (err, out) { // NOT OK
+ console.log(out);
+ });
+};
+
+var basePath = '/foo/bar';
+app.get('/:data', function (req, res) {
+ res.send(execSync('cat ' + basePath + req.params.data).toString()); // OK [but flagged] - caught by command-injection
+});
+
+spawn('cat', ['package.json']); // NOT OK
+
+spawn('/bin/cat', ['package.json']); // NOT
+
+spawn('/bin/cat', [someValue]); // NOT OK
+spawn('sudo /bin/cat', [someValue]); // NOT OK
+spawn('sudo cat', [someValue]); // NOT OK
+
+child_process.spawnSync('cat', [someValue]); // NOT OK
+
+child_process.execSync('cat package.json | wc -l'); // OK - pipes!
+
+const cat = child_process.spawn('cat') // OK - benign use (usually pipe to and from).
+
+spawn('cat', ['package.json']); // NOT OK.
+
+exec('cat *.js') // OK - wildcard use.
+exec('cat *.js | wc -l') // OK - wildcard use and pipes!
+exec('cat *.js | wc -l', { cwd: './' }, function () { }); // OK - wildcard and pipes
+
+spawnSync('cat', ['/proc/cpuinfo']) // NOT OK.
+
+exec(`cat ${path.join(__dirname, 'package.json')} | sort | uniq`, () => {}); // OK: pipes
+
+execSync('cat /proc/cpuinfo').toString(); // NOT OK.
+
+var cmd = "cat /proc/cpuinfo"
+execSync(cmd); // NOT OK
+
+execSync("cat /proc/cpuinfo | grep -c '" + someValue + "'"); // OK - pipes
+
+function cat(file) {
+ return execSync('cat ' + file).toString(); // NOT OK [flagged]
+}
+
+execSync(`cat ${files.join(' ')} > ${outFile}`); // NOT OK [flagged]
+
+var cmd = 'cat package.json | grep'
+exec(cmd); // OK - pipes!
+
+execSync("sudo cat " + newpath + "*.js | grep foo").toString(); // OK - wildcard and pipes
+
+execSync(`cat ${newpath}`); // NOT OK
+
+exec("cat /proc/cpuinfo | grep name"); // OK - pipes
+
+execSync(`cat ${newpath} | ${othertool}`); // OK - pipes
+
+execSync("sh -c 'cat " + newpath + "'"); // NOT OK. [but not flagged]
+
+exec(` cat ${newpath}`) // NOT OK
+
+exec(` cat ${newpath} | grep foo`) // OK - pipes
\ No newline at end of file
From 344060e1395d56b6d33a13bd68c91aa6f82063c8 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Wed, 19 Feb 2020 10:12:24 +0100
Subject: [PATCH 02/37] accept IO redirections as OK
---
.../javascript/security/UselessUseOfCat.qll | 4 ++--
.../Security/CWE-078/UselessCat.expected | 1 +
.../query-tests/Security/CWE-078/uselesscat.js | 15 ++++++++++++---
3 files changed, 15 insertions(+), 5 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index caf6c2a666ad..e5c5e8a0a1ab 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -36,8 +36,8 @@ class UselessCat extends DataFlow::Node {
commandString = getStartingString(command).trim() and
(commandString = cat or commandString.regexpMatch(cat + " .*"))
) and
- // `cat` is OK in combination with pipes and wildcards.
- not getAString(command).regexpMatch(".*(\\*|\\|).*") and
+ // `cat` is OK in combination with pipes, wildcards, and redirections.
+ not getAString(command).regexpMatch(".*(\\*|\\||>|<).*") and
// It is OK just to spawn "cat" without any arguments.
not (
command.mayHaveStringValue(cat) and
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.expected b/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.expected
index b19c48336703..67b59aa9d6ef 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.expected
@@ -1,2 +1,3 @@
| False negative | uselesscat.js:69:42:69:69 | // NOT ... lagged] |
| False positive | uselesscat.js:18:70:18:118 | // OK [ ... jection |
+| False positive | uselesscat.js:82:80:82:128 | // OK ( ... / gid)) |
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
index d613908e8d36..4fe7ccd75d2f 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
+++ b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
@@ -50,10 +50,10 @@ execSync(cmd); // NOT OK
execSync("cat /proc/cpuinfo | grep -c '" + someValue + "'"); // OK - pipes
function cat(file) {
- return execSync('cat ' + file).toString(); // NOT OK [flagged]
+ return execSync('cat ' + file).toString(); // NOT OK
}
-execSync(`cat ${files.join(' ')} > ${outFile}`); // NOT OK [flagged]
+execSync(`cat ${files.join(' ')} > ${outFile}`); // OK
var cmd = 'cat package.json | grep'
exec(cmd); // OK - pipes!
@@ -70,4 +70,13 @@ execSync("sh -c 'cat " + newpath + "'"); // NOT OK. [but not flagged]
exec(` cat ${newpath}`) // NOT OK
-exec(` cat ${newpath} | grep foo`) // OK - pipes
\ No newline at end of file
+exec(` cat ${newpath} | grep foo`) // OK - pipes
+
+execSync('cat /proc/cpuinfo > foo/bar/baz').toString(); // OK.
+
+execSync(`cat ${newpath} > ${destpath}`).toString(); // OK.
+
+const Opts = {encoding: 'utf8'}
+execSync(`cat foo/bar/${newpath}`, Opts).slice(0, 7); // NOT OK ("encoding" is used EXACTLY the same way in fs.readFileSync)
+
+execSync("/bin/cat /proc/cpuinfo", { uid: 1000, gid: 1000, encoding: 'utf8'}); // OK (fs.readFileSync cannot emulate uid / gid))
\ No newline at end of file
From bdab9ee12b44c303e086fb1bf950745a93dcaf6d Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Wed, 19 Feb 2020 14:31:03 +0100
Subject: [PATCH 03/37] change useless cat query to only flag instances that
can be re-written to
---
.../{UselessCat.ql => UselessUseOfCat.ql} | 3 +-
.../javascript/security/UselessUseOfCat.qll | 322 ++++++++++++++++--
.../Security/CWE-078/UselessCat.expected | 3 -
.../Security/CWE-078/UselessUseOfCat.expected | 20 ++
.../{UselessCat.ql => UselessUseOfCat.ql} | 2 +
.../Security/CWE-078/uselesscat.js | 94 ++---
6 files changed, 366 insertions(+), 78 deletions(-)
rename javascript/ql/src/Security/CWE-078/{UselessCat.ql => UselessUseOfCat.ql} (83%)
delete mode 100644 javascript/ql/test/query-tests/Security/CWE-078/UselessCat.expected
create mode 100644 javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
rename javascript/ql/test/query-tests/Security/CWE-078/{UselessCat.ql => UselessUseOfCat.ql} (89%)
diff --git a/javascript/ql/src/Security/CWE-078/UselessCat.ql b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
similarity index 83%
rename from javascript/ql/src/Security/CWE-078/UselessCat.ql
rename to javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
index d2f0e13fe2e1..f48ba6b8e958 100644
--- a/javascript/ql/src/Security/CWE-078/UselessCat.ql
+++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
@@ -14,7 +14,8 @@
import javascript
import semmle.javascript.security.UselessUseOfCat
+
from UselessCat cat
-select cat.getCommand(), "Useless use of `cat` in $@.", cat, "command execution"
+select cat, "Useless use of `cat`. Can be replaced with: " + createReadFileCall(cat)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index e5c5e8a0a1ab..f67fa2876853 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -6,54 +6,312 @@ import javascript
import semmle.javascript.security.dataflow.IndirectCommandArgument
/**
- * Gets the first string from a string/string-concatenation.
+ * Gets a string representing an equivalent call to `fs.ReadFile` for a call to `cat`.
*/
-private string getStartingString(DataFlow::Node node) {
- node.mayHaveStringValue(result) or
- node.(StringOps::ConcatenationRoot).getFirstLeaf().mayHaveStringValue(result)
+string createReadFileCall(UselsesCatCandidates::UselessCatCandicate cat) {
+ exists(string sync, string extraArg, string callback |
+ (if cat.isSync() then sync = "Sync" else sync = "") and
+ (
+ if exists(cat.getOptionsArg())
+ then extraArg = ", " + printOptionsArg(cat.getOptionsArg()) + ")"
+ else extraArg = ""
+ ) and
+ if exists(cat.getCallback())
+ then callback = ", function(" + getCallbackArgs(cat.getCallback()) + ") {...}"
+ else callback = ""
+ |
+ result = "fs.readFile" + sync + "(" + cat.getFileArgument().trim() + extraArg + callback + ")"
+ )
}
/**
- * Gets a string from a string/string-concatenation.
+ * Gets a string concatenation of the parameters to a function.
*/
-private string getAString(DataFlow::Node node) {
- node.mayHaveStringValue(result) or
- node.(StringOps::ConcatenationRoot).getALeaf().mayHaveStringValue(result)
+private string getCallbackArgs(DataFlow::FunctionNode func) {
+ result = concat(int i | i = [0 .. 2] | func.getParameter(i).getName(), ", ")
}
/**
- * An command-line execution of `cat` that only reads a file.
+ * Gets a string representation of the options argument from an exec-like call.
*/
-class UselessCat extends DataFlow::Node {
- DataFlow::Node command;
+private string printOptionsArg(DataFlow::Node node) {
+ result = node.asExpr().(VarAccess).getVariable().getName()
+ or
+ // fall back to toString(), but ensure that we don't have dots in the middle.
+ result = node.(DataFlow::ObjectLiteralNode).toString() and not result.regexpMatch(".*\\.\\..*")
+}
+
+/**
+ * A call to a useless use of `cat`.
+ */
+class UselessCat extends DataFlow::CallNode {
+ UselsesCatCandidates::UselessCatCandicate candidate;
UselessCat() {
- command = this.(SystemCommandExecution).getACommandArgument() and
- exists(string cat |
- cat = "cat" or cat = "/bin/cat" or cat = "sudo cat" or cat = "sudo /bin/cat"
- |
- exists(string commandString |
- commandString = getStartingString(command).trim() and
- (commandString = cat or commandString.regexpMatch(cat + " .*"))
- ) and
- // `cat` is OK in combination with pipes, wildcards, and redirections.
- not getAString(command).regexpMatch(".*(\\*|\\||>|<).*") and
- // It is OK just to spawn "cat" without any arguments.
- not (
- command.mayHaveStringValue(cat) and
- not exists(
- this
- .(SystemCommandExecution)
- .getArgumentList()
- .(DataFlow::ArrayCreationNode)
- .getAnElement()
+ this = candidate and
+ // wildcards, pipes, redirections, and multiple files are OK.
+ // (The multiple files detection relies on the fileArgument not containing spaces anywhere)
+ not candidate.getFileArgument().regexpMatch(".*(\\*|\\||>|<| ).*") and
+ // Only acceptable option is "encoding", everything else is non-trivial to emulate with fs.readFile.
+ not exists(string prop |
+ not prop = "encoding" and
+ exists(candidate.getOptionsArg().getALocalSource().getAPropertyWrite(prop))
+ ) and
+ exists(createReadFileCall(this)) and
+ // If there is a callback, then it must either have one or two arguments, or if there is a third argument it must be unused.
+ (
+ not exists(candidate.getCallback())
+ or
+ exists(DataFlow::FunctionNode func | func = candidate.getCallback() |
+ func.getNumParameter() = 1
+ or
+ func.getNumParameter() = 2
+ or
+ // `exec` can use 3 parameters, `readFile` can only use two, so it is OK to have a third parameter if it is unused,
+ func.getNumParameter() = 3 and
+ not exists(DataFlow::Node node |
+ not node = func.getParameter(2) and func.getParameter(2) = node.getALocalSource()
)
)
)
}
+}
+
+module UselsesCatCandidates {
+ /**
+ * A candidate for a useless use of `cat`.
+ * Subclasses of this class specify the structure of the `exec`-like call.
+ */
+ abstract class UselessCatCandicate extends DataFlow::CallNode {
+ /**
+ * Holds if the call is synchronous (e.g. `execFileSync`).
+ */
+ abstract predicate isSync();
+
+ /**
+ * Gets a string representation of the expression that determines what file is read.
+ */
+ abstract string getFileArgument();
+
+ /**
+ * Gets the data-flow node for the options argument to the `exec`-like call.
+ */
+ abstract DataFlow::Node getOptionsArg();
+
+ /**
+ * Gets the callback used for the `exec` like call (if it exists).
+ */
+ abstract DataFlow::FunctionNode getCallback();
+ }
+
+ /**
+ * Create a string representation of a string concatenation.
+ */
+ private string createConcatRepresentation(StringOps::ConcatenationRoot root) {
+ // String concat
+ not exists(root.getStringValue()) and
+ not root.asExpr() instanceof TemplateLiteral and
+ forall(Expr e | e = root.getALeaf().asExpr() | exists(createLeafRepresentation(e))) and
+ result =
+ concat(Expr leaf |
+ leaf = root.getALeaf().asExpr()
+ |
+ createLeafRepresentation(leaf), "+" order by leaf.getFirstToken().getIndex()
+ )
+ or
+ // Template string
+ exists(TemplateLiteral template | template = root.asExpr() |
+ forall(Expr e | e = template.getAChild() | exists(createTemplateElementRepresentation(e))) and
+ result =
+ "`" +
+ concat(int i |
+ i = [0 .. template.getNumChild() - 1]
+ |
+ createTemplateElementRepresentation(template.getChild(i)) order by i
+ ) + "`"
+ )
+ }
/**
- * Gets the dataflow node determining the command executed.
+ * Gets a string representing the expression needed to re-create the value for a leaf in a string-concatenation.
*/
- DataFlow::Node getCommand() { result = command }
+ private string createLeafRepresentation(Expr e) {
+ result = "\"" + e.getStringValue() + "\"" or
+ result = e.(VarAccess).getVariable().getName()
+ }
+
+ /**
+ * Gets a string representing the expression needed to re-create the value for an element of a template string.
+ */
+ private string createTemplateElementRepresentation(Expr e) {
+ result = "${" + e.(VarAccess).getVariable().getName() + "}"
+ or
+ result = e.(TemplateElement).getValue()
+ }
+
+ /**
+ * Gets a string used to call `cat`.
+ */
+ private string cat() {
+ result = "cat" or result = "/bin/cat" or result = "sudo cat" or result = "sudo /bin/cat"
+ }
+
+ /**
+ * Gets a string representing an expression that gets the file read by a call to `cat`.
+ * The input `arg` is the node that determines the commandline where `cat` is invoked.
+ */
+ private string getFileArgumentWithoutCat(DataFlow::Node arg) {
+ exists(string cat | cat = cat() |
+ exists(string command | arg.mayHaveStringValue(command) |
+ command.prefix(cat.length()) = cat and
+ result = "\"" + command.suffix(cat.length()).trim() + "\""
+ )
+ or
+ exists(StringOps::ConcatenationRoot root, string printed, string quote |
+ root = arg and printed = createConcatRepresentation(root).suffix(1) // remove initial quote
+ |
+ (if root.asExpr() instanceof TemplateLiteral then quote = "`" else quote = "\"") and
+ root.getFirstLeaf().getStringValue().prefix(cat.length()) = cat and
+ // Remove an initial ""+ (e.g. in `""+file`)
+ exists(string rawConcat | rawConcat = quote + printed.suffix(cat.length()).trim() |
+ if rawConcat.prefix(3) = "\"\"+" then result = rawConcat.suffix(3) else result = rawConcat
+ )
+ )
+ )
+ }
+
+ /**
+ * A call to child_process.exec that might be a useless call to cat.
+ */
+ private class ExecCall extends UselessCatCandicate {
+ string fileArgument;
+ boolean hasOptions;
+
+ ExecCall() {
+ this = DataFlow::moduleImport("child_process").getAMemberCall("exec") and
+ (
+ this.getNumArgument() = 2 and hasOptions = false
+ or
+ this.getNumArgument() = 3 and hasOptions = true
+ ) and
+ fileArgument = getFileArgumentWithoutCat(getArgument(0))
+ }
+
+ override predicate isSync() { none() }
+
+ override string getFileArgument() { result = fileArgument }
+
+ override DataFlow::Node getOptionsArg() { hasOptions = true and result = getArgument(1) }
+
+ override DataFlow::FunctionNode getCallback() { result = getLastArgument() }
+ }
+
+ /**
+ * A call to child_process.execSync that might be a useless call to cat.
+ */
+ private class ExecSyncCall extends UselessCatCandicate {
+ string fileArgument;
+ boolean hasOptions;
+
+ ExecSyncCall() {
+ this = DataFlow::moduleImport("child_process").getAMemberCall("execSync") and
+ (
+ this.getNumArgument() = 1 and hasOptions = false
+ or
+ this.getNumArgument() = 2 and hasOptions = true
+ ) and
+ fileArgument = getFileArgumentWithoutCat(getArgument(0))
+ }
+
+ override predicate isSync() { any() }
+
+ override string getFileArgument() { result = fileArgument }
+
+ override DataFlow::Node getOptionsArg() { hasOptions = true and result = getArgument(1) }
+
+ override DataFlow::FunctionNode getCallback() { none() }
+ }
+
+ // TODO: No!
+ bindingset[str, quote]
+ string surroundInQuotes(string str, string quote) {
+ if not str.prefix(1) = quote and not str.suffix(str.length() - 1) = quote
+ then result = quote + str + quote
+ else
+ if not str.prefix(1) = quote
+ then result = str + quote
+ else
+ if not str.suffix(str.length() - 1) = quote
+ then result = quote + str
+ else result = str
+ }
+
+ /**
+ * Gets the file that is read for a call to child_process.execFile/execFileSync.
+ */
+ string getFileThatIsRead(DataFlow::CallNode call) {
+ exists(DataFlow::ArrayCreationNode array, DataFlow::Node element |
+ array = call.getArgument(1).(DataFlow::ArrayCreationNode) and
+ array.getSize() = 1 and
+ element = array.getElement(0)
+ |
+ result = element.asExpr().(VarAccess).getVariable().getName() or
+ result = "\"" + element.getStringValue() + "\"" or
+ result = createConcatRepresentation(element)
+ )
+ }
+
+ /**
+ * A call to child_process.execFile that might be a useless call to cat.
+ */
+ private class ExecFileCall extends UselessCatCandicate {
+ string fileArgument;
+ boolean hasOptions;
+
+ ExecFileCall() {
+ this.getArgument(0).mayHaveStringValue(cat()) and
+ this = DataFlow::moduleImport("child_process").getAMemberCall("execFile") and
+ (
+ this.getNumArgument() = 3 and hasOptions = false
+ or
+ this.getNumArgument() = 4 and hasOptions = true
+ ) and
+ fileArgument = getFileThatIsRead(this)
+ }
+
+ override predicate isSync() { none() }
+
+ override string getFileArgument() { result = fileArgument }
+
+ override DataFlow::Node getOptionsArg() { hasOptions = true and result = getArgument(2) }
+
+ override DataFlow::FunctionNode getCallback() { result = getLastArgument() }
+ }
+
+ /**
+ * A call to child_process.execFileSync that might be a useless call to cat.
+ */
+ private class ExecFileSyncCall extends UselessCatCandicate {
+ string fileArgument;
+ boolean hasOptions;
+
+ ExecFileSyncCall() {
+ this.getArgument(0).mayHaveStringValue(cat()) and
+ this = DataFlow::moduleImport("child_process").getAMemberCall("execFileSync") and
+ (
+ this.getNumArgument() = 2 and hasOptions = false
+ or
+ this.getNumArgument() = 3 and hasOptions = true
+ ) and
+ fileArgument = getFileThatIsRead(this)
+ }
+
+ override predicate isSync() { any() }
+
+ override string getFileArgument() { result = fileArgument }
+
+ override DataFlow::Node getOptionsArg() { hasOptions = true and result = getArgument(2) }
+
+ override DataFlow::FunctionNode getCallback() { none() }
+ }
}
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.expected b/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.expected
deleted file mode 100644
index 67b59aa9d6ef..000000000000
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.expected
+++ /dev/null
@@ -1,3 +0,0 @@
-| False negative | uselesscat.js:69:42:69:69 | // NOT ... lagged] |
-| False positive | uselesscat.js:18:70:18:118 | // OK [ ... jection |
-| False positive | uselesscat.js:82:80:82:128 | // OK ( ... / gid)) |
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
new file mode 100644
index 000000000000..f4b22cd0803f
--- /dev/null
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
@@ -0,0 +1,20 @@
+readFile
+| uselesscat.js:10:1:10:43 | exec("c ... ut) {}) | fs.readFile("foo/bar", function(err, out) {...}) |
+| uselesscat.js:12:1:14:2 | exec("c ... ut);\\n}) | fs.readFile("/proc/"+id+"/status", function(err, out) {...}) |
+| uselesscat.js:16:1:16:29 | execSyn ... uinfo') | fs.readFileSync("/proc/cpuinfo") |
+| uselesscat.js:18:1:18:26 | execSyn ... path}`) | fs.readFileSync(`${newpath}`) |
+| uselesscat.js:32:1:32:34 | execSyn ... path}`) | fs.readFileSync(`foo/bar/${newpath}`) |
+| uselesscat.js:34:1:34:54 | execSyn ... utf8'}) | fs.readFileSync(`foo/bar/${newpath}`, {encoding: 'utf8'})) |
+| uselesscat.js:51:9:51:31 | execSyn ... + file) | fs.readFileSync(file) |
+| uselesscat.js:59:1:62:2 | execFil ... ut);\\n}) | fs.readFile("pom.xml", function(error, stderr, stdout) {...}) |
+| uselesscat.js:69:1:72:2 | execFil ... ut);\\n}) | fs.readFile("pom.xml", {encoding: 'utf8'}), function(error, stderr, stdout) {...}) |
+| uselesscat.js:74:1:74:60 | execFil ... utf8'}) | fs.readFileSync("pom.xml", {encoding: 'utf8'})) |
+| uselesscat.js:76:1:76:39 | execFil ... xml' ]) | fs.readFileSync("pom.xml") |
+| uselesscat.js:79:1:79:46 | execFil ... opts) | fs.readFileSync("pom.xml", opts)) |
+| uselesscat.js:82:1:82:90 | execFil ... String) | fs.readFileSync("pom.xml", anOptsFileNameThatIsTooLongToBePrintedByToString)) |
+| uselesscat.js:86:1:86:75 | execFil ... utf8'}) | fs.readFileSync("foo/"+newPath+"bar", {encoding: 'utf8'})) |
+| uselesscat.js:88:1:88:35 | execSyn ... + foo) | fs.readFileSync("/proc/cpuinfo"+foo) |
+| uselesscat.js:90:1:90:50 | execFil ... th}` ]) | fs.readFileSync(`foo/bar/${newpath}`) |
+#select
+| False negative | uselesscat.js:54:42:54:69 | // NOT ... lagged] |
+| False negative | uselesscat.js:84:118:84:144 | // NOT ... lagged] |
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.ql b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql
similarity index 89%
rename from javascript/ql/test/query-tests/Security/CWE-078/UselessCat.ql
rename to javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql
index bfb342f7ad15..edcf4987e3e8 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessCat.ql
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql
@@ -16,3 +16,5 @@ where
msg = "False positive"
)
select msg, comment
+
+query string readFile(UselessCat cat) { result = createReadFileCall(cat) }
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
index 4fe7ccd75d2f..d35f3224d189 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
+++ b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
@@ -7,76 +7,86 @@ var spawnSync = child_process.spawnSync;
var fs = require('fs');
var app = express();
-function readStatus(id) {
- exec("cat /proc/" + id + "/status", function (err, out) { // NOT OK
- console.log(out);
- });
-};
-
-var basePath = '/foo/bar';
-app.get('/:data', function (req, res) {
- res.send(execSync('cat ' + basePath + req.params.data).toString()); // OK [but flagged] - caught by command-injection
+exec("cat foo/bar", function (err, out) {}); // NOT OK
+
+exec("cat /proc/" + id + "/status", function (err, out) { // NOT OK
+ console.log(out);
});
-spawn('cat', ['package.json']); // NOT OK
+execSync('cat /proc/cpuinfo').toString(); // NOT OK.
-spawn('/bin/cat', ['package.json']); // NOT
+execSync(`cat ${newpath}`) // NOT OK
-spawn('/bin/cat', [someValue]); // NOT OK
-spawn('sudo /bin/cat', [someValue]); // NOT OK
-spawn('sudo cat', [someValue]); // NOT OK
+child_process.execSync('cat package.json | wc -l'); // OK - pipes!
-child_process.spawnSync('cat', [someValue]); // NOT OK
+execSync('cat /proc/cpuinfo /foo/bar').toString(); // OK multiple files.
-child_process.execSync('cat package.json | wc -l'); // OK - pipes!
+execSync(`cat ${newpath} /foo/bar`).toString(); // OK multiple files.
-const cat = child_process.spawn('cat') // OK - benign use (usually pipe to and from).
+exec(`cat ${newpath} | grep foo`, function (err, out) { }) // OK - pipes
-spawn('cat', ['package.json']); // NOT OK.
+execSync(`cat ${newpath}`, {uid: 1000}) // OK - non trivial options
-exec('cat *.js') // OK - wildcard use.
-exec('cat *.js | wc -l') // OK - wildcard use and pipes!
-exec('cat *.js | wc -l', { cwd: './' }, function () { }); // OK - wildcard and pipes
+exec('cat *.js | wc -l', { cwd: './' }, function (err, out) { }); // OK - wildcard and pipes
-spawnSync('cat', ['/proc/cpuinfo']) // NOT OK.
+execSync(`cat foo/bar/${newpath}`); // NOT OK ("encoding" is used EXACTLY the same way in fs.readFileSync)
-exec(`cat ${path.join(__dirname, 'package.json')} | sort | uniq`, () => {}); // OK: pipes
+execSync(`cat foo/bar/${newpath}`, {encoding: 'utf8'}); // NOT OK ("encoding" is used EXACTLY the same way in fs.readFileSync)
-execSync('cat /proc/cpuinfo').toString(); // NOT OK.
+execSync("/bin/cat /proc/cpuinfo", { uid: 1000, gid: 1000, encoding: 'utf8'}); // OK (fs.readFileSync cannot emulate uid / gid))
+
+execSync('cat /proc/cpuinfo > foo/bar/baz').toString(); // OK.
-var cmd = "cat /proc/cpuinfo"
-execSync(cmd); // NOT OK
+execSync(`cat ${newpath} > ${destpath}`).toString(); // OK.
+
+execSync(`cat ${files.join(' ')} > ${outFile}`); // OK
+
+execSync(`cat ${files.join(' ')}`); // OK - not just a simple file read
+
+exec("cat /proc/cpuinfo | grep name"); // OK - pipes
-execSync("cat /proc/cpuinfo | grep -c '" + someValue + "'"); // OK - pipes
+execSync(`cat ${newpath} | ${othertool}`); // OK - pipes
function cat(file) {
return execSync('cat ' + file).toString(); // NOT OK
}
-execSync(`cat ${files.join(' ')} > ${outFile}`); // OK
+execSync("sh -c 'cat " + newpath + "'"); // NOT OK. [but not flagged]
-var cmd = 'cat package.json | grep'
-exec(cmd); // OK - pipes!
+var execFile = child_process.execFile;
+var execFileSync = child_process.execFileSync;
-execSync("sudo cat " + newpath + "*.js | grep foo").toString(); // OK - wildcard and pipes
+execFile('/bin/cat', [ 'pom.xml' ], function(error, stdout, stderr ) { // NOT OK
+ // Not using stderr
+ console.log(stdout);
+});
-execSync(`cat ${newpath}`); // NOT OK
+execFile('/bin/cat', [ 'pom.xml' ], function(error, stdout, stderr ) { // OK. - stderr is used.
+ console.log(stderr);
+});
-exec("cat /proc/cpuinfo | grep name"); // OK - pipes
-execSync(`cat ${newpath} | ${othertool}`); // OK - pipes
+execFile('/bin/cat', [ 'pom.xml' ], {encoding: 'utf8'}, function(error, stdout, stderr ) { // NOT OK
+ // Not using stderr
+ console.log(stdout);
+});
-execSync("sh -c 'cat " + newpath + "'"); // NOT OK. [but not flagged]
+execFileSync('/bin/cat', [ 'pom.xml' ], {encoding: 'utf8'}); // NOT OK
-exec(` cat ${newpath}`) // NOT OK
+execFileSync('/bin/cat', [ 'pom.xml' ]); // NOT OK
-exec(` cat ${newpath} | grep foo`) // OK - pipes
+var opts = {encoding: 'utf8'};
+execFileSync('/bin/cat', [ 'pom.xml' ], opts); // NOT OK
-execSync('cat /proc/cpuinfo > foo/bar/baz').toString(); // OK.
+var anOptsFileNameThatIsTooLongToBePrintedByToString = {encoding: 'utf8'};
+execFileSync('/bin/cat', [ 'pom.xml' ], anOptsFileNameThatIsTooLongToBePrintedByToString); // NOT OK
-execSync(`cat ${newpath} > ${destpath}`).toString(); // OK.
+execFileSync('/bin/cat', [ 'pom.xml' ], {encoding: 'someEncodingValueThatIsCompletelyBogusAndTooLongForToString'}); // NOT OK [but not flagged]
+
+execFileSync('/bin/cat', [ "foo/" + newPath + "bar" ], {encoding: 'utf8'}); // NOT OK
+
+execSync('cat /proc/cpuinfo' + foo).toString(); // NOT OK.
-const Opts = {encoding: 'utf8'}
-execSync(`cat foo/bar/${newpath}`, Opts).slice(0, 7); // NOT OK ("encoding" is used EXACTLY the same way in fs.readFileSync)
+execFileSync('/bin/cat', [ `foo/bar/${newpath}` ]); // NOT OK
-execSync("/bin/cat /proc/cpuinfo", { uid: 1000, gid: 1000, encoding: 'utf8'}); // OK (fs.readFileSync cannot emulate uid / gid))
\ No newline at end of file
+execFileSync('node', [ `foo/bar/${newpath}` ]); // OK - not a call to cat
From 56f3e431f934952efa50deba7d2f913fbef88377 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Thu, 20 Feb 2020 10:28:53 +0100
Subject: [PATCH 04/37] update expected output
---
.../Security/CWE-078/CommandInjection.expected | 9 ---------
1 file changed, 9 deletions(-)
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection.expected b/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection.expected
index 3211c1196453..589deda96c96 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection.expected
@@ -89,10 +89,6 @@ nodes
| third-party-command-injection.js:5:20:5:26 | command |
| third-party-command-injection.js:6:21:6:27 | command |
| third-party-command-injection.js:6:21:6:27 | command |
-| uselesscat.js:18:20:18:54 | 'cat ' ... ms.data |
-| uselesscat.js:18:20:18:54 | 'cat ' ... ms.data |
-| uselesscat.js:18:40:18:54 | req.params.data |
-| uselesscat.js:18:40:18:54 | req.params.data |
edges
| child_process-test.js:6:9:6:49 | cmd | child_process-test.js:17:13:17:15 | cmd |
| child_process-test.js:6:9:6:49 | cmd | child_process-test.js:17:13:17:15 | cmd |
@@ -182,10 +178,6 @@ edges
| third-party-command-injection.js:5:20:5:26 | command | third-party-command-injection.js:6:21:6:27 | command |
| third-party-command-injection.js:5:20:5:26 | command | third-party-command-injection.js:6:21:6:27 | command |
| third-party-command-injection.js:5:20:5:26 | command | third-party-command-injection.js:6:21:6:27 | command |
-| uselesscat.js:18:40:18:54 | req.params.data | uselesscat.js:18:20:18:54 | 'cat ' ... ms.data |
-| uselesscat.js:18:40:18:54 | req.params.data | uselesscat.js:18:20:18:54 | 'cat ' ... ms.data |
-| uselesscat.js:18:40:18:54 | req.params.data | uselesscat.js:18:20:18:54 | 'cat ' ... ms.data |
-| uselesscat.js:18:40:18:54 | req.params.data | uselesscat.js:18:20:18:54 | 'cat ' ... ms.data |
#select
| child_process-test.js:17:13:17:15 | cmd | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:17:13:17:15 | cmd | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
| child_process-test.js:18:17:18:19 | cmd | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:18:17:18:19 | cmd | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
@@ -219,4 +211,3 @@ edges
| other.js:18:22:18:24 | cmd | other.js:5:25:5:31 | req.url | other.js:18:22:18:24 | cmd | This command depends on $@. | other.js:5:25:5:31 | req.url | a user-provided value |
| other.js:19:36:19:38 | cmd | other.js:5:25:5:31 | req.url | other.js:19:36:19:38 | cmd | This command depends on $@. | other.js:5:25:5:31 | req.url | a user-provided value |
| third-party-command-injection.js:6:21:6:27 | command | third-party-command-injection.js:5:20:5:26 | command | third-party-command-injection.js:6:21:6:27 | command | This command depends on $@. | third-party-command-injection.js:5:20:5:26 | command | a server-provided value |
-| uselesscat.js:18:20:18:54 | 'cat ' ... ms.data | uselesscat.js:18:40:18:54 | req.params.data | uselesscat.js:18:20:18:54 | 'cat ' ... ms.data | This command depends on $@. | uselesscat.js:18:40:18:54 | req.params.data | a user-provided value |
From d4e73df92f400fc6962363d594b1a4878d3aa87f Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Thu, 20 Feb 2020 10:39:16 +0100
Subject: [PATCH 05/37] remove dead predicate
---
.../semmle/javascript/security/UselessUseOfCat.qll | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index f67fa2876853..3afe8dda8e68 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -232,20 +232,6 @@ module UselsesCatCandidates {
override DataFlow::FunctionNode getCallback() { none() }
}
- // TODO: No!
- bindingset[str, quote]
- string surroundInQuotes(string str, string quote) {
- if not str.prefix(1) = quote and not str.suffix(str.length() - 1) = quote
- then result = quote + str + quote
- else
- if not str.prefix(1) = quote
- then result = str + quote
- else
- if not str.suffix(str.length() - 1) = quote
- then result = quote + str
- else result = str
- }
-
/**
* Gets the file that is read for a call to child_process.execFile/execFileSync.
*/
From a5fdcb67f9b2b96228b02caf59003f970ada2c46 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Thu, 20 Feb 2020 10:43:41 +0100
Subject: [PATCH 06/37] restricts alerts to the first line
---
javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
index f48ba6b8e958..e3fde814d666 100644
--- a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
+++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
@@ -13,9 +13,8 @@
import javascript
import semmle.javascript.security.UselessUseOfCat
+import semmle.javascript.RestrictedLocations
from UselessCat cat
-select cat, "Useless use of `cat`. Can be replaced with: " + createReadFileCall(cat)
-
-
+select cat.asExpr().(FirstLineOf), "Useless use of `cat`. Can be replaced with: " + createReadFileCall(cat)
\ No newline at end of file
From 558beb72559e8c6ee97609ffd3f29beaaf06bc0a Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Thu, 20 Feb 2020 10:57:33 +0100
Subject: [PATCH 07/37] simplify the output file argument
---
.../javascript/security/UselessUseOfCat.qll | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index 3afe8dda8e68..02eacb08a864 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -172,14 +172,28 @@ module UselsesCatCandidates {
|
(if root.asExpr() instanceof TemplateLiteral then quote = "`" else quote = "\"") and
root.getFirstLeaf().getStringValue().prefix(cat.length()) = cat and
- // Remove an initial ""+ (e.g. in `""+file`)
exists(string rawConcat | rawConcat = quote + printed.suffix(cat.length()).trim() |
- if rawConcat.prefix(3) = "\"\"+" then result = rawConcat.suffix(3) else result = rawConcat
+ result = getSimplifiedStringConcat(rawConcat)
)
)
)
}
+ /**
+ * Gets a simplified and equivalent string concatenation for a given string concatenation `str`
+ */
+ bindingset[str]
+ private string getSimplifiedStringConcat(string str) {
+ // Remove an initial ""+ (e.g. in `""+file`)
+ if str.prefix(3) = "\"\"+" then
+ result = str.suffix(3)
+ // prettify `${newpath}` to just newpath
+ else if str.prefix(3) = "`${" and str.suffix(str.length() - 2) = "}`" and not str.suffix(3).matches("%{%") then
+ result = str.prefix(str.length() - 2).suffix(3)
+ else
+ result = str
+ }
+
/**
* A call to child_process.exec that might be a useless call to cat.
*/
From a193cb110eb9960c56e93b9f3816e6d273077d88 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Thu, 20 Feb 2020 11:13:39 +0100
Subject: [PATCH 08/37] support arrow functions in the callbacks
---
.../javascript/security/UselessUseOfCat.qll | 28 ++++++++++++++-----
.../Security/CWE-078/UselessUseOfCat.expected | 5 +++-
.../Security/CWE-078/uselesscat.js | 6 ++++
3 files changed, 31 insertions(+), 8 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index 02eacb08a864..ac5338eb27ef 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -17,13 +17,24 @@ string createReadFileCall(UselsesCatCandidates::UselessCatCandicate cat) {
else extraArg = ""
) and
if exists(cat.getCallback())
- then callback = ", function(" + getCallbackArgs(cat.getCallback()) + ") {...}"
+ then callback = constructCallbackString(cat.getCallback())
else callback = ""
|
result = "fs.readFile" + sync + "(" + cat.getFileArgument().trim() + extraArg + callback + ")"
)
}
+string constructCallbackString(DataFlow::FunctionNode func) {
+ exists(string args | args = getCallbackArgs(func) |
+ if func.getFunction() instanceof ArrowFunctionExpr
+ then
+ if func.getFunction().getBody() instanceof Expr
+ then result = ", (" + args + ") => ..."
+ else result = ", (" + args + ") => {...}"
+ else result = ", function(" + args + ") {...}"
+ )
+}
+
/**
* Gets a string concatenation of the parameters to a function.
*/
@@ -185,13 +196,16 @@ module UselsesCatCandidates {
bindingset[str]
private string getSimplifiedStringConcat(string str) {
// Remove an initial ""+ (e.g. in `""+file`)
- if str.prefix(3) = "\"\"+" then
- result = str.suffix(3)
- // prettify `${newpath}` to just newpath
- else if str.prefix(3) = "`${" and str.suffix(str.length() - 2) = "}`" and not str.suffix(3).matches("%{%") then
- result = str.prefix(str.length() - 2).suffix(3)
+ if str.prefix(3) = "\"\"+"
+ then result = str.suffix(3)
else
- result = str
+ // prettify `${newpath}` to just newpath
+ if
+ str.prefix(3) = "`${" and
+ str.suffix(str.length() - 2) = "}`" and
+ not str.suffix(3).matches("%{%")
+ then result = str.prefix(str.length() - 2).suffix(3)
+ else result = str
}
/**
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
index f4b22cd0803f..de1c6aa86fed 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
@@ -2,7 +2,7 @@ readFile
| uselesscat.js:10:1:10:43 | exec("c ... ut) {}) | fs.readFile("foo/bar", function(err, out) {...}) |
| uselesscat.js:12:1:14:2 | exec("c ... ut);\\n}) | fs.readFile("/proc/"+id+"/status", function(err, out) {...}) |
| uselesscat.js:16:1:16:29 | execSyn ... uinfo') | fs.readFileSync("/proc/cpuinfo") |
-| uselesscat.js:18:1:18:26 | execSyn ... path}`) | fs.readFileSync(`${newpath}`) |
+| uselesscat.js:18:1:18:26 | execSyn ... path}`) | fs.readFileSync(newpath) |
| uselesscat.js:32:1:32:34 | execSyn ... path}`) | fs.readFileSync(`foo/bar/${newpath}`) |
| uselesscat.js:34:1:34:54 | execSyn ... utf8'}) | fs.readFileSync(`foo/bar/${newpath}`, {encoding: 'utf8'})) |
| uselesscat.js:51:9:51:31 | execSyn ... + file) | fs.readFileSync(file) |
@@ -15,6 +15,9 @@ readFile
| uselesscat.js:86:1:86:75 | execFil ... utf8'}) | fs.readFileSync("foo/"+newPath+"bar", {encoding: 'utf8'})) |
| uselesscat.js:88:1:88:35 | execSyn ... + foo) | fs.readFileSync("/proc/cpuinfo"+foo) |
| uselesscat.js:90:1:90:50 | execFil ... th}` ]) | fs.readFileSync(`foo/bar/${newpath}`) |
+| uselesscat.js:94:1:94:43 | exec("c ... ut) {}) | fs.readFile("foo/bar", function(err, out) {...}) |
+| uselesscat.js:96:1:96:53 | exec("c ... (out)}) | fs.readFile("foo/bar", (err, out) => {...}) |
+| uselesscat.js:98:1:98:55 | exec("c ... h(out)) | fs.readFile("foo/bar", (err, out) => ...) |
#select
| False negative | uselesscat.js:54:42:54:69 | // NOT ... lagged] |
| False negative | uselesscat.js:84:118:84:144 | // NOT ... lagged] |
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
index d35f3224d189..98c41513baa0 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
+++ b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
@@ -90,3 +90,9 @@ execSync('cat /proc/cpuinfo' + foo).toString(); // NOT OK.
execFileSync('/bin/cat', [ `foo/bar/${newpath}` ]); // NOT OK
execFileSync('node', [ `foo/bar/${newpath}` ]); // OK - not a call to cat
+
+exec("cat foo/bar", function (err, out) {}); // NOT OK
+
+exec("cat foo/bar", (err, out) => {console.log(out)}); // NOT OK
+
+exec("cat foo/bar", (err, out) => doSomethingWith(out)); // NOT OK
\ No newline at end of file
From b5ef45e6c232b163ea95dd8911f8b0a0c61d68ed Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Thu, 20 Feb 2020 11:30:23 +0100
Subject: [PATCH 09/37] add isSync predicate to SystemCommandExecution
---
.../ql/src/semmle/javascript/Concepts.qll | 3 ++
.../javascript/frameworks/NodeJSLib.qll | 4 ++
.../semmle/javascript/frameworks/ShellJS.qll | 2 +
.../frameworks/SystemCommandExecutors.qll | 20 +++++++-
.../Security/CWE-078/UselessUseOfCat.expected | 46 +++++++++++++++++++
.../Security/CWE-078/UselessUseOfCat.ql | 4 ++
6 files changed, 78 insertions(+), 1 deletion(-)
diff --git a/javascript/ql/src/semmle/javascript/Concepts.qll b/javascript/ql/src/semmle/javascript/Concepts.qll
index 23bbbce3b163..4fef87f85854 100644
--- a/javascript/ql/src/semmle/javascript/Concepts.qll
+++ b/javascript/ql/src/semmle/javascript/Concepts.qll
@@ -22,6 +22,9 @@ abstract class SystemCommandExecution extends DataFlow::Node {
* to the command.
*/
DataFlow::Node getArgumentList() { none() }
+
+ /** Holds if the command execution happens synchronously. */
+ abstract predicate isSync();
}
/**
diff --git a/javascript/ql/src/semmle/javascript/frameworks/NodeJSLib.qll b/javascript/ql/src/semmle/javascript/frameworks/NodeJSLib.qll
index 0ee7c10fa7ff..95600e072f66 100644
--- a/javascript/ql/src/semmle/javascript/frameworks/NodeJSLib.qll
+++ b/javascript/ql/src/semmle/javascript/frameworks/NodeJSLib.qll
@@ -623,6 +623,10 @@ module NodeJSLib {
// all of the above methods take the argument list as their second argument
result = getArgument(1)
}
+
+ override predicate isSync() {
+ "Sync" = methodName.suffix(methodName.length() - 4)
+ }
}
/**
diff --git a/javascript/ql/src/semmle/javascript/frameworks/ShellJS.qll b/javascript/ql/src/semmle/javascript/frameworks/ShellJS.qll
index 226054792444..e459fe9089c7 100644
--- a/javascript/ql/src/semmle/javascript/frameworks/ShellJS.qll
+++ b/javascript/ql/src/semmle/javascript/frameworks/ShellJS.qll
@@ -160,6 +160,8 @@ module ShellJS {
override DataFlow::Node getACommandArgument() { result = getArgument(0) }
override predicate isShellInterpreted(DataFlow::Node arg) { arg = getACommandArgument() }
+
+ override predicate isSync() {none ()}
}
/**
diff --git a/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll b/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll
index 7cea87bb3f3a..e0664f93dd1a 100644
--- a/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll
+++ b/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll
@@ -9,6 +9,7 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
int cmdArg;
boolean shell;
+ boolean sync;
SystemCommandExecutors() {
exists(string mod, DataFlow::SourceNode callee |
@@ -31,9 +32,11 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
) and
cmdArg = 0
|
- callee = DataFlow::moduleMember(mod, method)
+ callee = DataFlow::moduleMember(mod, method) and
+ sync = getSync(method)
)
or
+ sync = false and
(
shell = false and
(
@@ -65,4 +68,19 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
override predicate isShellInterpreted(DataFlow::Node arg) {
arg = getACommandArgument() and shell = true
}
+
+ override predicate isSync() {
+ sync = true
+ }
}
+
+/**
+ * Gets a boolean reflecting if the name ends with "sync" or "Sync".
+ */
+bindingset[name]
+private boolean getSync(string name) {
+ if name.suffix(name.length() - 4) = "Sync" or name.suffix(name.length() - 4) = "sync" then
+ result = true
+ else
+ result = false
+}
\ No newline at end of file
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
index de1c6aa86fed..3acd23863d0e 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
@@ -18,6 +18,52 @@ readFile
| uselesscat.js:94:1:94:43 | exec("c ... ut) {}) | fs.readFile("foo/bar", function(err, out) {...}) |
| uselesscat.js:96:1:96:53 | exec("c ... (out)}) | fs.readFile("foo/bar", (err, out) => {...}) |
| uselesscat.js:98:1:98:55 | exec("c ... h(out)) | fs.readFile("foo/bar", (err, out) => ...) |
+syncCommand
+| child_process-test.js:9:5:9:22 | cp.execSync("foo") |
+| child_process-test.js:11:5:11:26 | cp.exec ... ("foo") |
+| child_process-test.js:13:5:13:23 | cp.spawnSync("foo") |
+| child_process-test.js:18:5:18:20 | cp.execSync(cmd) |
+| child_process-test.js:20:5:20:24 | cp.execFileSync(cmd) |
+| child_process-test.js:22:5:22:21 | cp.spawnSync(cmd) |
+| command-line-parameter-command-injection.js:11:2:11:21 | cp.execSync(args[0]) |
+| command-line-parameter-command-injection.js:12:2:12:33 | cp.exec ... rgs[0]) |
+| command-line-parameter-command-injection.js:15:2:15:26 | cp.exec ... rgs[0]) |
+| command-line-parameter-command-injection.js:16:2:16:38 | cp.exec ... rgs[0]) |
+| command-line-parameter-command-injection.js:19:2:19:18 | cp.execSync(arg0) |
+| command-line-parameter-command-injection.js:20:2:20:30 | cp.exec ... + arg0) |
+| command-line-parameter-command-injection.js:26:2:26:51 | cp.exec ... tion"`) |
+| command-line-parameter-command-injection.js:27:2:27:58 | cp.exec ... tion"`) |
+| other.js:7:5:7:36 | require ... nc(cmd) |
+| other.js:9:5:9:35 | require ... nc(cmd) |
+| other.js:12:5:12:30 | require ... nc(cmd) |
+| third-party-command-injection.js:6:9:6:28 | cp.execSync(command) |
+| tst_shell-command-injection-from-environment.js:4:2:4:62 | cp.exec ... emp")]) |
+| tst_shell-command-injection-from-environment.js:5:2:5:54 | cp.exec ... temp")) |
+| uselesscat.js:16:1:16:29 | execSyn ... uinfo') |
+| uselesscat.js:18:1:18:26 | execSyn ... path}`) |
+| uselesscat.js:20:1:20:50 | child_p ... wc -l') |
+| uselesscat.js:22:1:22:38 | execSyn ... o/bar') |
+| uselesscat.js:24:1:24:35 | execSyn ... o/bar`) |
+| uselesscat.js:28:1:28:39 | execSyn ... 1000}) |
+| uselesscat.js:32:1:32:34 | execSyn ... path}`) |
+| uselesscat.js:34:1:34:54 | execSyn ... utf8'}) |
+| uselesscat.js:36:1:36:77 | execSyn ... utf8'}) |
+| uselesscat.js:38:1:38:43 | execSyn ... r/baz') |
+| uselesscat.js:40:1:40:40 | execSyn ... path}`) |
+| uselesscat.js:42:1:42:47 | execSyn ... File}`) |
+| uselesscat.js:44:1:44:34 | execSyn ... ' ')}`) |
+| uselesscat.js:48:1:48:41 | execSyn ... tool}`) |
+| uselesscat.js:51:9:51:31 | execSyn ... + file) |
+| uselesscat.js:54:1:54:39 | execSyn ... + "'") |
+| uselesscat.js:74:1:74:60 | execFil ... utf8'}) |
+| uselesscat.js:76:1:76:39 | execFil ... xml' ]) |
+| uselesscat.js:79:1:79:46 | execFil ... opts) |
+| uselesscat.js:82:1:82:90 | execFil ... String) |
+| uselesscat.js:84:1:84:115 | execFil ... ring'}) |
+| uselesscat.js:86:1:86:75 | execFil ... utf8'}) |
+| uselesscat.js:88:1:88:35 | execSyn ... + foo) |
+| uselesscat.js:90:1:90:50 | execFil ... th}` ]) |
+| uselesscat.js:92:1:92:46 | execFil ... th}` ]) |
#select
| False negative | uselesscat.js:54:42:54:69 | // NOT ... lagged] |
| False negative | uselesscat.js:84:118:84:144 | // NOT ... lagged] |
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql
index edcf4987e3e8..f9a4fd2ea8c2 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql
@@ -18,3 +18,7 @@ where
select msg, comment
query string readFile(UselessCat cat) { result = createReadFileCall(cat) }
+
+query SystemCommandExecution syncCommand() {
+ result.isSync()
+}
\ No newline at end of file
From 12c0291dde44e65f39db09e6918eba6ee09b86bd Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Thu, 20 Feb 2020 11:35:11 +0100
Subject: [PATCH 10/37] require that an options object has a known set of
properties
---
.../semmle/javascript/security/UselessUseOfCat.qll | 13 +++++++++----
.../Security/CWE-078/UselessUseOfCat.expected | 1 +
.../test/query-tests/Security/CWE-078/uselesscat.js | 4 +++-
3 files changed, 13 insertions(+), 5 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index ac5338eb27ef..5558fdcb654a 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -60,15 +60,20 @@ class UselessCat extends DataFlow::CallNode {
UselessCat() {
this = candidate and
+ exists(createReadFileCall(this)) and
// wildcards, pipes, redirections, and multiple files are OK.
// (The multiple files detection relies on the fileArgument not containing spaces anywhere)
not candidate.getFileArgument().regexpMatch(".*(\\*|\\||>|<| ).*") and
// Only acceptable option is "encoding", everything else is non-trivial to emulate with fs.readFile.
- not exists(string prop |
- not prop = "encoding" and
- exists(candidate.getOptionsArg().getALocalSource().getAPropertyWrite(prop))
+ (
+ not exists(candidate.getOptionsArg())
+ or
+ forex(string prop |
+ exists(candidate.getOptionsArg().getALocalSource().getAPropertyWrite(prop))
+ |
+ prop = "encoding"
+ )
) and
- exists(createReadFileCall(this)) and
// If there is a callback, then it must either have one or two arguments, or if there is a third argument it must be unused.
(
not exists(candidate.getCallback())
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
index 3acd23863d0e..61def6726b38 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
@@ -64,6 +64,7 @@ syncCommand
| uselesscat.js:88:1:88:35 | execSyn ... + foo) |
| uselesscat.js:90:1:90:50 | execFil ... th}` ]) |
| uselesscat.js:92:1:92:46 | execFil ... th}` ]) |
+| uselesscat.js:100:1:100:56 | execFil ... ptions) |
#select
| False negative | uselesscat.js:54:42:54:69 | // NOT ... lagged] |
| False negative | uselesscat.js:84:118:84:144 | // NOT ... lagged] |
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
index 98c41513baa0..c860d21f6549 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
+++ b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
@@ -95,4 +95,6 @@ exec("cat foo/bar", function (err, out) {}); // NOT OK
exec("cat foo/bar", (err, out) => {console.log(out)}); // NOT OK
-exec("cat foo/bar", (err, out) => doSomethingWith(out)); // NOT OK
\ No newline at end of file
+exec("cat foo/bar", (err, out) => doSomethingWith(out)); // NOT OK
+
+execFileSync('/bin/cat', [ 'pom.xml' ], unknownOptions); // OK - unknown options.
\ No newline at end of file
From b1cbfce50bdc96b53ea7feb5c34ca3f64604892e Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Thu, 20 Feb 2020 14:17:37 +0100
Subject: [PATCH 11/37] use SystemCommandExecution and a few small fixes
---
.../javascript/security/UselessUseOfCat.qll | 180 ++++++------------
.../Security/CWE-078/UselessUseOfCat.expected | 4 +-
2 files changed, 57 insertions(+), 127 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index 5558fdcb654a..a77298f83767 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -3,10 +3,9 @@
*/
import javascript
-import semmle.javascript.security.dataflow.IndirectCommandArgument
/**
- * Gets a string representing an equivalent call to `fs.ReadFile` for a call to `cat`.
+ * Gets a string representation of an equivalent call to `fs.readFile` for a given command execution `cat`.
*/
string createReadFileCall(UselsesCatCandidates::UselessCatCandicate cat) {
exists(string sync, string extraArg, string callback |
@@ -24,6 +23,9 @@ string createReadFileCall(UselsesCatCandidates::UselessCatCandicate cat) {
)
}
+/**
+ * Constructs a string representing the callback `func`.
+ */
string constructCallbackString(DataFlow::FunctionNode func) {
exists(string args | args = getCallbackArgs(func) |
if func.getFunction() instanceof ArrowFunctionExpr
@@ -36,20 +38,25 @@ string constructCallbackString(DataFlow::FunctionNode func) {
}
/**
- * Gets a string concatenation of the parameters to a function.
+ * Gets a string concatenation of the parameter names in a function `func`.
*/
private string getCallbackArgs(DataFlow::FunctionNode func) {
- result = concat(int i | i = [0 .. 2] | func.getParameter(i).getName(), ", ")
+ result =
+ concat(int i |
+ i = [0 .. func.getNumParameter()]
+ |
+ func.getParameter(i).getName(), ", " order by i
+ )
}
/**
- * Gets a string representation of the options argument from an exec-like call.
+ * Gets a string representation of the options argument `arg` from an exec-like call.
*/
-private string printOptionsArg(DataFlow::Node node) {
- result = node.asExpr().(VarAccess).getVariable().getName()
+private string printOptionsArg(DataFlow::Node arg) {
+ result = arg.asExpr().(VarAccess).getVariable().getName()
or
// fall back to toString(), but ensure that we don't have dots in the middle.
- result = node.(DataFlow::ObjectLiteralNode).toString() and not result.regexpMatch(".*\\.\\..*")
+ result = arg.(DataFlow::ObjectLiteralNode).toString() and not result.regexpMatch(".*\\.\\..*")
}
/**
@@ -60,6 +67,7 @@ class UselessCat extends DataFlow::CallNode {
UselessCat() {
this = candidate and
+ // We can create an equivalent `fs.readFile` call.
exists(createReadFileCall(this)) and
// wildcards, pipes, redirections, and multiple files are OK.
// (The multiple files detection relies on the fileArgument not containing spaces anywhere)
@@ -96,28 +104,56 @@ class UselessCat extends DataFlow::CallNode {
module UselsesCatCandidates {
/**
* A candidate for a useless use of `cat`.
- * Subclasses of this class specify the structure of the `exec`-like call.
+ * This class describes the structure of a call to cat
+ * This class does not determine whether it is a useless call, or even if it is a call to `cat`.
*/
- abstract class UselessCatCandicate extends DataFlow::CallNode {
+ class UselessCatCandicate extends DataFlow::CallNode {
+ SystemCommandExecution command;
+
+ UselessCatCandicate() { this = command }
+
/**
* Holds if the call is synchronous (e.g. `execFileSync`).
*/
- abstract predicate isSync();
+ predicate isSync() { command.isSync() }
/**
- * Gets a string representation of the expression that determines what file is read.
+ * Holds if the executed command execution has an argument list as a separate argument.
*/
- abstract string getFileArgument();
+ predicate hasArgumentList() { exists(command.getArgumentList()) }
/**
- * Gets the data-flow node for the options argument to the `exec`-like call.
+ * Gets a string representation of the expression that determines what file is read by `cat`.
*/
- abstract DataFlow::Node getOptionsArg();
+ string getFileArgument() {
+ if hasArgumentList()
+ then
+ getArgument(0).mayHaveStringValue(cat()) and
+ result = getFileThatIsReadFromCommandList(this)
+ else result = getFileArgumentWithoutCat(getArgument(0))
+ }
/**
- * Gets the callback used for the `exec` like call (if it exists).
+ * Gets the data-flow node (if it exists) for the options argument to the `exec`-like call.
*/
- abstract DataFlow::FunctionNode getCallback();
+ DataFlow::Node getOptionsArg() {
+ exists(int n |
+ n >= 1 and
+ // If there is a command-list, then the options is at least the third argument.
+ (not exists(command.getArgumentList()) or n >= 2) and
+ // async calls have a callback as their last call.
+ if this.isSync() then n < getNumArgument() else n < getNumArgument() - 1
+ |
+ result = getArgument(n)
+ )
+ }
+
+ /**
+ * Gets the callback (if it exists) for an async `exec` like call.
+ */
+ DataFlow::FunctionNode getCallback() {
+ not this.isSync() and result = getLastArgument().getALocalSource()
+ }
}
/**
@@ -214,61 +250,9 @@ module UselsesCatCandidates {
}
/**
- * A call to child_process.exec that might be a useless call to cat.
- */
- private class ExecCall extends UselessCatCandicate {
- string fileArgument;
- boolean hasOptions;
-
- ExecCall() {
- this = DataFlow::moduleImport("child_process").getAMemberCall("exec") and
- (
- this.getNumArgument() = 2 and hasOptions = false
- or
- this.getNumArgument() = 3 and hasOptions = true
- ) and
- fileArgument = getFileArgumentWithoutCat(getArgument(0))
- }
-
- override predicate isSync() { none() }
-
- override string getFileArgument() { result = fileArgument }
-
- override DataFlow::Node getOptionsArg() { hasOptions = true and result = getArgument(1) }
-
- override DataFlow::FunctionNode getCallback() { result = getLastArgument() }
- }
-
- /**
- * A call to child_process.execSync that might be a useless call to cat.
- */
- private class ExecSyncCall extends UselessCatCandicate {
- string fileArgument;
- boolean hasOptions;
-
- ExecSyncCall() {
- this = DataFlow::moduleImport("child_process").getAMemberCall("execSync") and
- (
- this.getNumArgument() = 1 and hasOptions = false
- or
- this.getNumArgument() = 2 and hasOptions = true
- ) and
- fileArgument = getFileArgumentWithoutCat(getArgument(0))
- }
-
- override predicate isSync() { any() }
-
- override string getFileArgument() { result = fileArgument }
-
- override DataFlow::Node getOptionsArg() { hasOptions = true and result = getArgument(1) }
-
- override DataFlow::FunctionNode getCallback() { none() }
- }
-
- /**
- * Gets the file that is read for a call to child_process.execFile/execFileSync.
+ * Gets the file that is read for a call with an explicit command list (e.g. `child_process.execFile/execFileSync`).
*/
- string getFileThatIsRead(DataFlow::CallNode call) {
+ string getFileThatIsReadFromCommandList(DataFlow::CallNode call) {
exists(DataFlow::ArrayCreationNode array, DataFlow::Node element |
array = call.getArgument(1).(DataFlow::ArrayCreationNode) and
array.getSize() = 1 and
@@ -279,58 +263,4 @@ module UselsesCatCandidates {
result = createConcatRepresentation(element)
)
}
-
- /**
- * A call to child_process.execFile that might be a useless call to cat.
- */
- private class ExecFileCall extends UselessCatCandicate {
- string fileArgument;
- boolean hasOptions;
-
- ExecFileCall() {
- this.getArgument(0).mayHaveStringValue(cat()) and
- this = DataFlow::moduleImport("child_process").getAMemberCall("execFile") and
- (
- this.getNumArgument() = 3 and hasOptions = false
- or
- this.getNumArgument() = 4 and hasOptions = true
- ) and
- fileArgument = getFileThatIsRead(this)
- }
-
- override predicate isSync() { none() }
-
- override string getFileArgument() { result = fileArgument }
-
- override DataFlow::Node getOptionsArg() { hasOptions = true and result = getArgument(2) }
-
- override DataFlow::FunctionNode getCallback() { result = getLastArgument() }
- }
-
- /**
- * A call to child_process.execFileSync that might be a useless call to cat.
- */
- private class ExecFileSyncCall extends UselessCatCandicate {
- string fileArgument;
- boolean hasOptions;
-
- ExecFileSyncCall() {
- this.getArgument(0).mayHaveStringValue(cat()) and
- this = DataFlow::moduleImport("child_process").getAMemberCall("execFileSync") and
- (
- this.getNumArgument() = 2 and hasOptions = false
- or
- this.getNumArgument() = 3 and hasOptions = true
- ) and
- fileArgument = getFileThatIsRead(this)
- }
-
- override predicate isSync() { any() }
-
- override string getFileArgument() { result = fileArgument }
-
- override DataFlow::Node getOptionsArg() { hasOptions = true and result = getArgument(2) }
-
- override DataFlow::FunctionNode getCallback() { none() }
- }
}
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
index 61def6726b38..f4ebbd0848ba 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
@@ -6,8 +6,8 @@ readFile
| uselesscat.js:32:1:32:34 | execSyn ... path}`) | fs.readFileSync(`foo/bar/${newpath}`) |
| uselesscat.js:34:1:34:54 | execSyn ... utf8'}) | fs.readFileSync(`foo/bar/${newpath}`, {encoding: 'utf8'})) |
| uselesscat.js:51:9:51:31 | execSyn ... + file) | fs.readFileSync(file) |
-| uselesscat.js:59:1:62:2 | execFil ... ut);\\n}) | fs.readFile("pom.xml", function(error, stderr, stdout) {...}) |
-| uselesscat.js:69:1:72:2 | execFil ... ut);\\n}) | fs.readFile("pom.xml", {encoding: 'utf8'}), function(error, stderr, stdout) {...}) |
+| uselesscat.js:59:1:62:2 | execFil ... ut);\\n}) | fs.readFile("pom.xml", function(error, stdout, stderr) {...}) |
+| uselesscat.js:69:1:72:2 | execFil ... ut);\\n}) | fs.readFile("pom.xml", {encoding: 'utf8'}), function(error, stdout, stderr) {...}) |
| uselesscat.js:74:1:74:60 | execFil ... utf8'}) | fs.readFileSync("pom.xml", {encoding: 'utf8'})) |
| uselesscat.js:76:1:76:39 | execFil ... xml' ]) | fs.readFileSync("pom.xml") |
| uselesscat.js:79:1:79:46 | execFil ... opts) | fs.readFileSync("pom.xml", opts)) |
From b2ccec28e076c3b93c56da15cce11acd3577f748 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Thu, 20 Feb 2020 14:34:50 +0100
Subject: [PATCH 12/37] require the file to be non-empty
---
.../ql/src/semmle/javascript/security/UselessUseOfCat.qll | 2 ++
1 file changed, 2 insertions(+)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index a77298f83767..47a99b0bf4e5 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -69,6 +69,8 @@ class UselessCat extends DataFlow::CallNode {
this = candidate and
// We can create an equivalent `fs.readFile` call.
exists(createReadFileCall(this)) and
+ // There is a file to read, and not just a pair of quotes.
+ candidate.getFileArgument().length() >= 3 and
// wildcards, pipes, redirections, and multiple files are OK.
// (The multiple files detection relies on the fileArgument not containing spaces anywhere)
not candidate.getFileArgument().regexpMatch(".*(\\*|\\||>|<| ).*") and
From 924272a7a508567b3622a1a317e3649bc2424a2d Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Thu, 20 Feb 2020 14:35:26 +0100
Subject: [PATCH 13/37] insert placeholder qhelp
---
.../Security/CWE-078/UselessUseOfCat.qhelp | 30 +++++++++++++++++++
1 file changed, 30 insertions(+)
create mode 100644 javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
diff --git a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
new file mode 100644
index 000000000000..452f630712d2
--- /dev/null
+++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
@@ -0,0 +1,30 @@
+
+
+
+
+ Useless use of cat
+
+
+
+
+
+
+ TODO: This is a placeholder
+
+
+
+
+
+
+
+
+
+
+ OWASP:
+ Command Injection.
+
+
+
+
From 6ea14532ab155a8d7cc0cb2ab2f7c8552212e3f6 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Fri, 21 Feb 2020 10:27:57 +0100
Subject: [PATCH 14/37] small changes based on review
---
.../src/Security/CWE-078/UselessUseOfCat.ql | 5 ++---
.../javascript/security/UselessUseOfCat.qll | 19 +++++++++----------
2 files changed, 11 insertions(+), 13 deletions(-)
diff --git a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
index e3fde814d666..417b9d9f2e3e 100644
--- a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
+++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
@@ -1,14 +1,13 @@
/**
* @name Useless use of cat
- * @description Using cat to simply read a file can lead to unintended bugs, and at worst security issues.
+ * @description Using `cat`-process to simply read a file is unnecessarily complex, inefficient, unportable, can lead to subtle bugs, or even security vulnerabilities.
* @kind problem
* @problem.severity error
* @precision high
* @id js/useless-use-of-cat
* @tags correctness
* security
- * external/cwe/cwe-078
- * external/cwe/cwe-088
+ * maintainability
*/
import javascript
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index 47a99b0bf4e5..ca21f5d0bc65 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -11,13 +11,15 @@ string createReadFileCall(UselsesCatCandidates::UselessCatCandicate cat) {
exists(string sync, string extraArg, string callback |
(if cat.isSync() then sync = "Sync" else sync = "") and
(
- if exists(cat.getOptionsArg())
- then extraArg = ", " + printOptionsArg(cat.getOptionsArg()) + ")"
- else extraArg = ""
+ extraArg = ", " + printOptionsArg(cat.getOptionsArg()) + ")"
+ or
+ extraArg = "" and not exists(cat.getOptionsArg())
) and
- if exists(cat.getCallback())
- then callback = constructCallbackString(cat.getCallback())
- else callback = ""
+ (
+ callback = constructCallbackString(cat.getCallback())
+ or
+ callback = "" and not exists(cat.getCallback())
+ )
|
result = "fs.readFile" + sync + "(" + cat.getFileArgument().trim() + extraArg + callback + ")"
)
@@ -94,10 +96,7 @@ class UselessCat extends DataFlow::CallNode {
func.getNumParameter() = 2
or
// `exec` can use 3 parameters, `readFile` can only use two, so it is OK to have a third parameter if it is unused,
- func.getNumParameter() = 3 and
- not exists(DataFlow::Node node |
- not node = func.getParameter(2) and func.getParameter(2) = node.getALocalSource()
- )
+ func.getNumParameter() = 3 and not exists(SSA::definition(func.getParameter(2).getParameter()))
)
)
}
From 75410e5760c47f77313715f07ec333ca0f7083d1 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Fri, 21 Feb 2020 14:26:42 +0100
Subject: [PATCH 15/37] big refactor of UselessUseOfCal
---
.../src/Security/CWE-078/UselessUseOfCat.ql | 8 +-
.../javascript/security/UselessUseOfCat.qll | 320 +++++++++++-------
.../Security/CWE-078/UselessUseOfCat.expected | 11 +-
.../Security/CWE-078/UselessUseOfCat.ql | 2 +-
.../Security/CWE-078/uselesscat.js | 40 ++-
5 files changed, 244 insertions(+), 137 deletions(-)
diff --git a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
index 417b9d9f2e3e..4f080711719b 100644
--- a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
+++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
@@ -15,5 +15,9 @@ import semmle.javascript.security.UselessUseOfCat
import semmle.javascript.RestrictedLocations
-from UselessCat cat
-select cat.asExpr().(FirstLineOf), "Useless use of `cat`. Can be replaced with: " + createReadFileCall(cat)
\ No newline at end of file
+from UselessCat cat, string message
+where
+ message = " Can be replaced with: " + PrettyPrintCatCall::createReadFileCall(cat)
+ or
+ not exists(PrettyPrintCatCall::createReadFileCall(cat)) and message = ""
+select cat.asExpr().(FirstLineOf), "Useless use of `cat`." + message
\ No newline at end of file
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index ca21f5d0bc65..2633c84ce70c 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -1,160 +1,233 @@
/**
- * Provides predicates and classes for working with useless uses of `cat`.
+ * Provides predicates and classes for working with useless uses of the unix command `cat`.
*/
import javascript
+import Expressions.ExprHasNoEffect
+import Declarations.UnusedVariable
/**
- * Gets a string representation of an equivalent call to `fs.readFile` for a given command execution `cat`.
+ * A call that executes a system command.
+ * This class provide utility predicates for reasoning about command execution calls.
*/
-string createReadFileCall(UselsesCatCandidates::UselessCatCandicate cat) {
- exists(string sync, string extraArg, string callback |
- (if cat.isSync() then sync = "Sync" else sync = "") and
- (
- extraArg = ", " + printOptionsArg(cat.getOptionsArg()) + ")"
- or
- extraArg = "" and not exists(cat.getOptionsArg())
- ) and
- (
- callback = constructCallbackString(cat.getCallback())
- or
- callback = "" and not exists(cat.getCallback())
- )
- |
- result = "fs.readFile" + sync + "(" + cat.getFileArgument().trim() + extraArg + callback + ")"
- )
-}
+private class CommandCall extends DataFlow::InvokeNode {
+ SystemCommandExecution command;
-/**
- * Constructs a string representing the callback `func`.
- */
-string constructCallbackString(DataFlow::FunctionNode func) {
- exists(string args | args = getCallbackArgs(func) |
- if func.getFunction() instanceof ArrowFunctionExpr
- then
- if func.getFunction().getBody() instanceof Expr
- then result = ", (" + args + ") => ..."
- else result = ", (" + args + ") => {...}"
- else result = ", function(" + args + ") {...}"
- )
-}
+ CommandCall() { this = command }
-/**
- * Gets a string concatenation of the parameter names in a function `func`.
- */
-private string getCallbackArgs(DataFlow::FunctionNode func) {
- result =
- concat(int i |
- i = [0 .. func.getNumParameter()]
+ /**
+ * Holds if the call is synchronous (e.g. `execFileSync`).
+ */
+ predicate isSync() { command.isSync() }
+
+ /**
+ * Gets an argument to this command execution that specifies the argument list to the command.
+ */
+ DataFlow::Node getArgumentList() { result = command.getArgumentList() }
+
+ /**
+ * Gets the callback (if it exists) for an async `exec`-like call.
+ */
+ DataFlow::FunctionNode getCallback() {
+ not this.isSync() and result = getLastArgument().getALocalSource()
+ }
+
+ /**
+ * Holds if the executed command execution has an argument list as a separate argument.
+ */
+ predicate hasArgumentList() { exists(command.getArgumentList()) }
+
+ /**
+ * Gets the data-flow node (if it exists) for a options argument for an `exec`-like call.
+ */
+ DataFlow::Node getOptionsArg() {
+ exists(int n |
+ n >= 1 and
+ // if there is a command-list, then the options is at least the third argument.
+ (not exists(command.getArgumentList()) or n >= 2) and
+ // async exec calls can have a callback as their last call.
+ if command.isSync() or not exists(getCallback())
+ then n < getNumArgument()
+ else n < getNumArgument() - 1
|
- func.getParameter(i).getName(), ", " order by i
+ result = getArgument(n)
)
+ or
+ // Fallback in case normal API conventions are broken.
+ result = getAnArgument() and
+ result.getALocalSource() instanceof DataFlow::ObjectLiteralNode
+ }
+
+ /**
+ * Gets the constant-string parts that are not part of the command itself.
+ * E.g. for a command execution `exec("/bin/cat foo bar")` this predicate will have result `"foo bar"`.
+ */
+ string getNonCommandConstantString() {
+ if this.hasArgumentList()
+ then
+ result =
+ getConstantStringParts(getArgumentList()
+ .getALocalSource()
+ .(DataFlow::ArrayCreationNode)
+ .getElement(_))
+ else
+ exists(string commandString | commandString = getConstantStringParts(getArgument(0)) |
+ result = commandString.suffix(1 + commandString.indexOf(" ", 0, 0))
+ )
+ }
+
+ /**
+ * Holds if this command execution invokes the executeable `name`.
+ */
+ bindingset[name]
+ predicate isACallTo(string name) {
+ if this.hasArgumentList()
+ then getArgument(0).mayHaveStringValue(name)
+ else
+ exists(string arg | arg = getConstantStringParts(getArgument(0)) |
+ arg.prefix(name.length()) = name
+ )
+ }
}
/**
- * Gets a string representation of the options argument `arg` from an exec-like call.
+ * Gets the constant string parts from a data-flow node.
+ * Either the string is some constant
*/
-private string printOptionsArg(DataFlow::Node arg) {
- result = arg.asExpr().(VarAccess).getVariable().getName()
+private string getConstantStringParts(DataFlow::Node node) {
+ node.mayHaveStringValue(result)
or
- // fall back to toString(), but ensure that we don't have dots in the middle.
- result = arg.(DataFlow::ObjectLiteralNode).toString() and not result.regexpMatch(".*\\.\\..*")
+ result = node.(StringOps::ConcatenationRoot).getConstantStringParts()
}
/**
* A call to a useless use of `cat`.
*/
-class UselessCat extends DataFlow::CallNode {
- UselsesCatCandidates::UselessCatCandicate candidate;
-
+class UselessCat extends CommandCall {
UselessCat() {
- this = candidate and
- // We can create an equivalent `fs.readFile` call.
- exists(createReadFileCall(this)) and
+ this = command and
+ isACallTo(getACatExecuteable()) and
// There is a file to read, and not just a pair of quotes.
- candidate.getFileArgument().length() >= 3 and
- // wildcards, pipes, redirections, and multiple files are OK.
- // (The multiple files detection relies on the fileArgument not containing spaces anywhere)
- not candidate.getFileArgument().regexpMatch(".*(\\*|\\||>|<| ).*") and
+ (
+ not exists(PrettyPrintCatCall::createFileArgument(this))
+ or
+ exists(string fileArg | fileArg = PrettyPrintCatCall::createFileArgument(this) |
+ fileArg.length() >= 3
+ )
+ ) and
+ // wildcards, pipes, redirections, other bash features, and multiple files (spaces) are OK.
+ not getNonCommandConstantString().regexpMatch(".*(\\*|\\||>|<| |\\$|&|,|\\`).*") and
// Only acceptable option is "encoding", everything else is non-trivial to emulate with fs.readFile.
(
- not exists(candidate.getOptionsArg())
+ not exists(getOptionsArg())
or
- forex(string prop |
- exists(candidate.getOptionsArg().getALocalSource().getAPropertyWrite(prop))
- |
+ forex(string prop | exists(getOptionsArg().getALocalSource().getAPropertyWrite(prop)) |
prop = "encoding"
)
) and
// If there is a callback, then it must either have one or two arguments, or if there is a third argument it must be unused.
(
- not exists(candidate.getCallback())
+ not exists(getCallback())
or
- exists(DataFlow::FunctionNode func | func = candidate.getCallback() |
+ exists(DataFlow::FunctionNode func | func = getCallback() |
func.getNumParameter() = 1
or
func.getNumParameter() = 2
or
// `exec` can use 3 parameters, `readFile` can only use two, so it is OK to have a third parameter if it is unused,
- func.getNumParameter() = 3 and not exists(SSA::definition(func.getParameter(2).getParameter()))
+ func.getNumParameter() = 3 and
+ not exists(SSA::definition(func.getParameter(2).getParameter()))
)
+ ) and
+ // The process returned by an async call is unused.
+ (
+ isSync()
+ or
+ inVoidContext(this.getEnclosingExpr())
+ or
+ this.getEnclosingExpr() = any(UnusedLocal v).getAnAssignedExpr()
)
}
}
-module UselsesCatCandidates {
+/**
+ * Gets a string used to call `cat`.
+ */
+string getACatExecuteable() {
+ result = "cat" or result = "/bin/cat" or result = "sudo cat" or result = "sudo /bin/cat"
+}
+
+/**
+ * Predicates for creating an equivalent call to `fs.readFile` from a command execution of `cat`.
+ */
+module PrettyPrintCatCall {
/**
- * A candidate for a useless use of `cat`.
- * This class describes the structure of a call to cat
- * This class does not determine whether it is a useless call, or even if it is a call to `cat`.
+ * Create a string representation of an equivalent call to `fs.readFile` for a given command execution `cat`.
*/
- class UselessCatCandicate extends DataFlow::CallNode {
- SystemCommandExecution command;
-
- UselessCatCandicate() { this = command }
-
- /**
- * Holds if the call is synchronous (e.g. `execFileSync`).
- */
- predicate isSync() { command.isSync() }
+ string createReadFileCall(UselessCat cat) {
+ exists(string sync, string extraArg, string callback |
+ (if cat.isSync() then sync = "Sync" else sync = "") and
+ (
+ extraArg = ", " + createOptionsArg(cat.getOptionsArg()) + ")"
+ or
+ extraArg = "" and not exists(cat.getOptionsArg())
+ ) and
+ (
+ callback = createCallbackString(cat.getCallback())
+ or
+ callback = "" and not exists(cat.getCallback())
+ )
+ |
+ result =
+ "fs.readFile" + sync + "(" + createFileArgument(cat).trim() + extraArg + callback + ")"
+ )
+ }
- /**
- * Holds if the executed command execution has an argument list as a separate argument.
- */
- predicate hasArgumentList() { exists(command.getArgumentList()) }
+ /**
+ * Create a string representation of the expression that determines what file is read by `cat`.
+ */
+ string createFileArgument(CommandCall cat) {
+ if cat.hasArgumentList()
+ then
+ cat.getArgument(0).mayHaveStringValue(getACatExecuteable()) and
+ result = createFileThatIsReadFromCommandList(cat)
+ else result = createFileArgumentWithoutCat(cat.getArgument(0))
+ }
- /**
- * Gets a string representation of the expression that determines what file is read by `cat`.
- */
- string getFileArgument() {
- if hasArgumentList()
+ /**
+ * Create a string representing the callback `func`.
+ */
+ string createCallbackString(DataFlow::FunctionNode func) {
+ exists(string args | args = createCallbackArgs(func) |
+ if func.getFunction() instanceof ArrowFunctionExpr
then
- getArgument(0).mayHaveStringValue(cat()) and
- result = getFileThatIsReadFromCommandList(this)
- else result = getFileArgumentWithoutCat(getArgument(0))
- }
+ if func.getFunction().getBody() instanceof Expr
+ then result = ", (" + args + ") => ..."
+ else result = ", (" + args + ") => {...}"
+ else result = ", function(" + args + ") {...}"
+ )
+ }
- /**
- * Gets the data-flow node (if it exists) for the options argument to the `exec`-like call.
- */
- DataFlow::Node getOptionsArg() {
- exists(int n |
- n >= 1 and
- // If there is a command-list, then the options is at least the third argument.
- (not exists(command.getArgumentList()) or n >= 2) and
- // async calls have a callback as their last call.
- if this.isSync() then n < getNumArgument() else n < getNumArgument() - 1
+ /**
+ * Create a string concatenation of the parameter names in a function `func`.
+ */
+ private string createCallbackArgs(DataFlow::FunctionNode func) {
+ result =
+ concat(int i |
+ i = [0 .. func.getNumParameter()]
|
- result = getArgument(n)
+ func.getParameter(i).getName(), ", " order by i
)
- }
+ }
- /**
- * Gets the callback (if it exists) for an async `exec` like call.
- */
- DataFlow::FunctionNode getCallback() {
- not this.isSync() and result = getLastArgument().getALocalSource()
- }
+ /**
+ * Create a string representation of the options argument `arg` from an exec-like call.
+ */
+ private string createOptionsArg(DataFlow::Node arg) {
+ result = arg.asExpr().(VarAccess).getVariable().getName()
+ or
+ // fall back to toString(), but ensure that we don't have dots in the middle.
+ result = arg.(DataFlow::ObjectLiteralNode).toString() and not result.regexpMatch(".*\\.\\..*")
}
/**
@@ -169,7 +242,7 @@ module UselsesCatCandidates {
concat(Expr leaf |
leaf = root.getALeaf().asExpr()
|
- createLeafRepresentation(leaf), "+" order by leaf.getFirstToken().getIndex()
+ createLeafRepresentation(leaf), " + " order by leaf.getFirstToken().getIndex()
)
or
// Template string
@@ -186,7 +259,7 @@ module UselsesCatCandidates {
}
/**
- * Gets a string representing the expression needed to re-create the value for a leaf in a string-concatenation.
+ * Create a string representing the expression needed to re-create the value for a leaf in a string-concatenation.
*/
private string createLeafRepresentation(Expr e) {
result = "\"" + e.getStringValue() + "\"" or
@@ -194,7 +267,7 @@ module UselsesCatCandidates {
}
/**
- * Gets a string representing the expression needed to re-create the value for an element of a template string.
+ * Create a string representing the expression needed to re-create the value for an element of a template string.
*/
private string createTemplateElementRepresentation(Expr e) {
result = "${" + e.(VarAccess).getVariable().getName() + "}"
@@ -203,18 +276,11 @@ module UselsesCatCandidates {
}
/**
- * Gets a string used to call `cat`.
- */
- private string cat() {
- result = "cat" or result = "/bin/cat" or result = "sudo cat" or result = "sudo /bin/cat"
- }
-
- /**
- * Gets a string representing an expression that gets the file read by a call to `cat`.
+ * Create a string representing an expression that gets the file read by a call to `cat`.
* The input `arg` is the node that determines the commandline where `cat` is invoked.
*/
- private string getFileArgumentWithoutCat(DataFlow::Node arg) {
- exists(string cat | cat = cat() |
+ private string createFileArgumentWithoutCat(DataFlow::Node arg) {
+ exists(string cat | cat = getACatExecuteable() |
exists(string command | arg.mayHaveStringValue(command) |
command.prefix(cat.length()) = cat and
result = "\"" + command.suffix(cat.length()).trim() + "\""
@@ -226,20 +292,20 @@ module UselsesCatCandidates {
(if root.asExpr() instanceof TemplateLiteral then quote = "`" else quote = "\"") and
root.getFirstLeaf().getStringValue().prefix(cat.length()) = cat and
exists(string rawConcat | rawConcat = quote + printed.suffix(cat.length()).trim() |
- result = getSimplifiedStringConcat(rawConcat)
+ result = createSimplifiedStringConcat(rawConcat)
)
)
)
}
/**
- * Gets a simplified and equivalent string concatenation for a given string concatenation `str`
+ * Create a simplified and equivalent string concatenation for a given string concatenation `str`
*/
bindingset[str]
- private string getSimplifiedStringConcat(string str) {
+ private string createSimplifiedStringConcat(string str) {
// Remove an initial ""+ (e.g. in `""+file`)
- if str.prefix(3) = "\"\"+"
- then result = str.suffix(3)
+ if str.prefix(5) = "\"\" + "
+ then result = str.suffix(5)
else
// prettify `${newpath}` to just newpath
if
@@ -251,11 +317,11 @@ module UselsesCatCandidates {
}
/**
- * Gets the file that is read for a call with an explicit command list (e.g. `child_process.execFile/execFileSync`).
+ * Create the file that is read for a call with an explicit command list (e.g. `child_process.execFile/execFileSync`).
*/
- string getFileThatIsReadFromCommandList(DataFlow::CallNode call) {
+ string createFileThatIsReadFromCommandList(CommandCall call) {
exists(DataFlow::ArrayCreationNode array, DataFlow::Node element |
- array = call.getArgument(1).(DataFlow::ArrayCreationNode) and
+ array = call.getArgumentList().(DataFlow::ArrayCreationNode) and
array.getSize() = 1 and
element = array.getElement(0)
|
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
index f4ebbd0848ba..1490b8abb015 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
@@ -1,6 +1,6 @@
readFile
| uselesscat.js:10:1:10:43 | exec("c ... ut) {}) | fs.readFile("foo/bar", function(err, out) {...}) |
-| uselesscat.js:12:1:14:2 | exec("c ... ut);\\n}) | fs.readFile("/proc/"+id+"/status", function(err, out) {...}) |
+| uselesscat.js:12:1:14:2 | exec("c ... ut);\\n}) | fs.readFile("/proc/" + id + "/status", function(err, out) {...}) |
| uselesscat.js:16:1:16:29 | execSyn ... uinfo') | fs.readFileSync("/proc/cpuinfo") |
| uselesscat.js:18:1:18:26 | execSyn ... path}`) | fs.readFileSync(newpath) |
| uselesscat.js:32:1:32:34 | execSyn ... path}`) | fs.readFileSync(`foo/bar/${newpath}`) |
@@ -12,12 +12,14 @@ readFile
| uselesscat.js:76:1:76:39 | execFil ... xml' ]) | fs.readFileSync("pom.xml") |
| uselesscat.js:79:1:79:46 | execFil ... opts) | fs.readFileSync("pom.xml", opts)) |
| uselesscat.js:82:1:82:90 | execFil ... String) | fs.readFileSync("pom.xml", anOptsFileNameThatIsTooLongToBePrintedByToString)) |
-| uselesscat.js:86:1:86:75 | execFil ... utf8'}) | fs.readFileSync("foo/"+newPath+"bar", {encoding: 'utf8'})) |
-| uselesscat.js:88:1:88:35 | execSyn ... + foo) | fs.readFileSync("/proc/cpuinfo"+foo) |
+| uselesscat.js:86:1:86:75 | execFil ... utf8'}) | fs.readFileSync("foo/" + newPath + "bar", {encoding: 'utf8'})) |
+| uselesscat.js:88:1:88:35 | execSyn ... + foo) | fs.readFileSync("/proc/cpuinfo" + foo) |
| uselesscat.js:90:1:90:50 | execFil ... th}` ]) | fs.readFileSync(`foo/bar/${newpath}`) |
| uselesscat.js:94:1:94:43 | exec("c ... ut) {}) | fs.readFile("foo/bar", function(err, out) {...}) |
| uselesscat.js:96:1:96:53 | exec("c ... (out)}) | fs.readFile("foo/bar", (err, out) => {...}) |
| uselesscat.js:98:1:98:55 | exec("c ... h(out)) | fs.readFile("foo/bar", (err, out) => ...) |
+| uselesscat.js:121:12:121:64 | exec("c ... (out)}) | fs.readFile("foo/bar", (err, out) => {...}) |
+| uselesscat.js:127:14:127:66 | exec("c ... (out)}) | fs.readFile("foo/bar", (err, out) => {...}) |
syncCommand
| child_process-test.js:9:5:9:22 | cp.execSync("foo") |
| child_process-test.js:11:5:11:26 | cp.exec ... ("foo") |
@@ -65,6 +67,7 @@ syncCommand
| uselesscat.js:90:1:90:50 | execFil ... th}` ]) |
| uselesscat.js:92:1:92:46 | execFil ... th}` ]) |
| uselesscat.js:100:1:100:56 | execFil ... ptions) |
+| uselesscat.js:104:1:104:31 | execFil ... cat` ]) |
#select
| False negative | uselesscat.js:54:42:54:69 | // NOT ... lagged] |
-| False negative | uselesscat.js:84:118:84:144 | // NOT ... lagged] |
+| False positive | uselesscat.js:44:37:44:85 | // OK [ ... le read |
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql
index f9a4fd2ea8c2..a287a57ff551 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql
@@ -17,7 +17,7 @@ where
)
select msg, comment
-query string readFile(UselessCat cat) { result = createReadFileCall(cat) }
+query string readFile(UselessCat cat) { result = PrettyPrintCatCall::createReadFileCall(cat) }
query SystemCommandExecution syncCommand() {
result.isSync()
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
index c860d21f6549..d64058c010df 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
+++ b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
@@ -41,7 +41,7 @@ execSync(`cat ${newpath} > ${destpath}`).toString(); // OK.
execSync(`cat ${files.join(' ')} > ${outFile}`); // OK
-execSync(`cat ${files.join(' ')}`); // OK - not just a simple file read
+execSync(`cat ${files.join(' ')}`); // OK [but flagged] - not just a simple file read
exec("cat /proc/cpuinfo | grep name"); // OK - pipes
@@ -81,7 +81,7 @@ execFileSync('/bin/cat', [ 'pom.xml' ], opts); // NOT OK
var anOptsFileNameThatIsTooLongToBePrintedByToString = {encoding: 'utf8'};
execFileSync('/bin/cat', [ 'pom.xml' ], anOptsFileNameThatIsTooLongToBePrintedByToString); // NOT OK
-execFileSync('/bin/cat', [ 'pom.xml' ], {encoding: 'someEncodingValueThatIsCompletelyBogusAndTooLongForToString'}); // NOT OK [but not flagged]
+execFileSync('/bin/cat', [ 'pom.xml' ], {encoding: 'someEncodingValueThatIsCompletelyBogusAndTooLongForToString'}); // NOT OK
execFileSync('/bin/cat', [ "foo/" + newPath + "bar" ], {encoding: 'utf8'}); // NOT OK
@@ -97,4 +97,38 @@ exec("cat foo/bar", (err, out) => {console.log(out)}); // NOT OK
exec("cat foo/bar", (err, out) => doSomethingWith(out)); // NOT OK
-execFileSync('/bin/cat', [ 'pom.xml' ], unknownOptions); // OK - unknown options.
\ No newline at end of file
+execFileSync('/bin/cat', [ 'pom.xml' ], unknownOptions); // OK - unknown options.
+
+exec("node foo/bar", (err, out) => doSomethingWith(out)); // OK - Not a call to cat
+
+execFileSync('node', [ `cat` ]); // OK - not a call to cat
+
+exec("cat foo/bar&", function (err, out) {}); // OK - contains &
+exec("cat foo/bar,", function (err, out) {}); // OK - contains ,
+exec("cat foo/bar$", function (err, out) {}); // OK - contains $
+exec("cat foo/bar`", function (err, out) {}); // OK - contains `
+
+spawn('cat', { stdio: ['pipe', stdin, 'inherit'] }); // OK - Non trivial use. (But weird API use.)
+
+(function () {
+ const cat = spawn('cat', [filename]); // OK - non trivial use.
+ cat.stdout.on('data', (data) => {
+ res.write(data);
+ });
+ cat.stdout.on('end', () => res.end());
+})();
+
+var dead = exec("cat foo/bar", (err, out) => {console.log(out)}); // NOT OK
+
+var notDead = exec("cat foo/bar", (err, out) => {console.log(out)}); // OK
+console.log(notDead);
+
+(function () {
+ var dead = exec("cat foo/bar", (err, out) => {console.log(out)}); // NOT OK
+
+ someCall(
+ exec("cat foo/bar", (err, out) => {console.log(out)}) // OK - non-trivial use of returned proccess.
+ );
+
+ return exec("cat foo/bar", (err, out) => {console.log(out)}); // OK - non-trivial use of returned proccess.
+})();
\ No newline at end of file
From 44db0f4e5df8f0d1ae1d0c578583474c13487dde Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Fri, 21 Feb 2020 15:39:49 +0100
Subject: [PATCH 16/37] better printing of the options arg
---
.../javascript/security/UselessUseOfCat.qll | 14 ++++++++++----
.../Security/CWE-078/UselessUseOfCat.expected | 15 +++++++++------
.../query-tests/Security/CWE-078/uselesscat.js | 6 +++++-
3 files changed, 24 insertions(+), 11 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index 2633c84ce70c..1d2a13d960d0 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -59,9 +59,9 @@ private class CommandCall extends DataFlow::InvokeNode {
}
/**
- * Gets the constant-string parts that are not part of the command itself.
- * E.g. for a command execution `exec("/bin/cat foo bar")` this predicate will have result `"foo bar"`.
- */
+ * Gets the constant-string parts that are not part of the command itself.
+ * E.g. for a command execution `exec("/bin/cat foo bar")` this predicate will have result `"foo bar"`.
+ */
string getNonCommandConstantString() {
if this.hasArgumentList()
then
@@ -168,7 +168,13 @@ module PrettyPrintCatCall {
exists(string sync, string extraArg, string callback |
(if cat.isSync() then sync = "Sync" else sync = "") and
(
- extraArg = ", " + createOptionsArg(cat.getOptionsArg()) + ")"
+ exists(cat.getOptionsArg()) and
+ (
+ extraArg = ", " + createOptionsArg(cat.getOptionsArg())
+ or
+ not exists(createOptionsArg(cat.getOptionsArg())) and
+ extraArg = ", ..."
+ )
or
extraArg = "" and not exists(cat.getOptionsArg())
) and
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
index 1490b8abb015..c0de47ed9957 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
@@ -4,15 +4,16 @@ readFile
| uselesscat.js:16:1:16:29 | execSyn ... uinfo') | fs.readFileSync("/proc/cpuinfo") |
| uselesscat.js:18:1:18:26 | execSyn ... path}`) | fs.readFileSync(newpath) |
| uselesscat.js:32:1:32:34 | execSyn ... path}`) | fs.readFileSync(`foo/bar/${newpath}`) |
-| uselesscat.js:34:1:34:54 | execSyn ... utf8'}) | fs.readFileSync(`foo/bar/${newpath}`, {encoding: 'utf8'})) |
+| uselesscat.js:34:1:34:54 | execSyn ... utf8'}) | fs.readFileSync(`foo/bar/${newpath}`, {encoding: 'utf8'}) |
| uselesscat.js:51:9:51:31 | execSyn ... + file) | fs.readFileSync(file) |
| uselesscat.js:59:1:62:2 | execFil ... ut);\\n}) | fs.readFile("pom.xml", function(error, stdout, stderr) {...}) |
-| uselesscat.js:69:1:72:2 | execFil ... ut);\\n}) | fs.readFile("pom.xml", {encoding: 'utf8'}), function(error, stdout, stderr) {...}) |
-| uselesscat.js:74:1:74:60 | execFil ... utf8'}) | fs.readFileSync("pom.xml", {encoding: 'utf8'})) |
+| uselesscat.js:69:1:72:2 | execFil ... ut);\\n}) | fs.readFile("pom.xml", {encoding: 'utf8'}, function(error, stdout, stderr) {...}) |
+| uselesscat.js:74:1:74:60 | execFil ... utf8'}) | fs.readFileSync("pom.xml", {encoding: 'utf8'}) |
| uselesscat.js:76:1:76:39 | execFil ... xml' ]) | fs.readFileSync("pom.xml") |
-| uselesscat.js:79:1:79:46 | execFil ... opts) | fs.readFileSync("pom.xml", opts)) |
-| uselesscat.js:82:1:82:90 | execFil ... String) | fs.readFileSync("pom.xml", anOptsFileNameThatIsTooLongToBePrintedByToString)) |
-| uselesscat.js:86:1:86:75 | execFil ... utf8'}) | fs.readFileSync("foo/" + newPath + "bar", {encoding: 'utf8'})) |
+| uselesscat.js:79:1:79:46 | execFil ... opts) | fs.readFileSync("pom.xml", opts) |
+| uselesscat.js:82:1:82:90 | execFil ... String) | fs.readFileSync("pom.xml", anOptsFileNameThatIsTooLongToBePrintedByToString) |
+| uselesscat.js:84:1:84:115 | execFil ... ring'}) | fs.readFileSync("pom.xml", ...) |
+| uselesscat.js:86:1:86:75 | execFil ... utf8'}) | fs.readFileSync("foo/" + newPath + "bar", {encoding: 'utf8'}) |
| uselesscat.js:88:1:88:35 | execSyn ... + foo) | fs.readFileSync("/proc/cpuinfo" + foo) |
| uselesscat.js:90:1:90:50 | execFil ... th}` ]) | fs.readFileSync(`foo/bar/${newpath}`) |
| uselesscat.js:94:1:94:43 | exec("c ... ut) {}) | fs.readFile("foo/bar", function(err, out) {...}) |
@@ -20,6 +21,7 @@ readFile
| uselesscat.js:98:1:98:55 | exec("c ... h(out)) | fs.readFile("foo/bar", (err, out) => ...) |
| uselesscat.js:121:12:121:64 | exec("c ... (out)}) | fs.readFile("foo/bar", (err, out) => {...}) |
| uselesscat.js:127:14:127:66 | exec("c ... (out)}) | fs.readFile("foo/bar", (err, out) => {...}) |
+| uselesscat.js:136:17:138:2 | execSyn ... tf8'\\n}) | fs.readFileSync("/etc/dnsmasq.conf", ...) |
syncCommand
| child_process-test.js:9:5:9:22 | cp.execSync("foo") |
| child_process-test.js:11:5:11:26 | cp.exec ... ("foo") |
@@ -68,6 +70,7 @@ syncCommand
| uselesscat.js:92:1:92:46 | execFil ... th}` ]) |
| uselesscat.js:100:1:100:56 | execFil ... ptions) |
| uselesscat.js:104:1:104:31 | execFil ... cat` ]) |
+| uselesscat.js:136:17:138:2 | execSyn ... tf8'\\n}) |
#select
| False negative | uselesscat.js:54:42:54:69 | // NOT ... lagged] |
| False positive | uselesscat.js:44:37:44:85 | // OK [ ... le read |
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
index d64058c010df..c50e0fc1f5ee 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
+++ b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
@@ -131,4 +131,8 @@ console.log(notDead);
);
return exec("cat foo/bar", (err, out) => {console.log(out)}); // OK - non-trivial use of returned proccess.
-})();
\ No newline at end of file
+})();
+
+const stdout2 = execSync('cat /etc/dnsmasq.conf', { // NOT OK.
+ encoding: 'utf8'
+});
\ No newline at end of file
From 75c1852ee49e04aa5ddd9d57c5b9613ebbf2628b Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Mon, 24 Feb 2020 11:58:59 +0100
Subject: [PATCH 17/37] doc changes from review
Co-Authored-By: Esben Sparre Andreasen
---
.../ql/src/semmle/javascript/security/UselessUseOfCat.qll | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index 1d2a13d960d0..cccf7937c558 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -8,7 +8,7 @@ import Declarations.UnusedVariable
/**
* A call that executes a system command.
- * This class provide utility predicates for reasoning about command execution calls.
+ * This class provides utility predicates for reasoning about command execution calls.
*/
private class CommandCall extends DataFlow::InvokeNode {
SystemCommandExecution command;
@@ -38,7 +38,7 @@ private class CommandCall extends DataFlow::InvokeNode {
predicate hasArgumentList() { exists(command.getArgumentList()) }
/**
- * Gets the data-flow node (if it exists) for a options argument for an `exec`-like call.
+ * Gets the data-flow node (if it exists) for an options argument for an `exec`-like call.
*/
DataFlow::Node getOptionsArg() {
exists(int n |
From 473787a4268a5e90b4a653ee8bcdb3681f7867ff Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Mon, 24 Feb 2020 12:59:20 +0100
Subject: [PATCH 18/37] refactor the getOptionsArg predicate into the
SystemCommandExecution class
---
.../ql/src/semmle/javascript/Concepts.qll | 5 ++++
.../javascript/frameworks/NodeJSLib.qll | 12 ++++++++
.../semmle/javascript/frameworks/ShellJS.qll | 7 +++++
.../frameworks/SystemCommandExecutors.qll | 29 ++++++++++++-------
.../javascript/security/UselessUseOfCat.qll | 16 +---------
.../Security/CWE-078/UselessUseOfCat.expected | 17 +++++++++++
.../Security/CWE-078/UselessUseOfCat.ql | 4 +++
7 files changed, 65 insertions(+), 25 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/Concepts.qll b/javascript/ql/src/semmle/javascript/Concepts.qll
index 4fef87f85854..798747af8b8e 100644
--- a/javascript/ql/src/semmle/javascript/Concepts.qll
+++ b/javascript/ql/src/semmle/javascript/Concepts.qll
@@ -25,6 +25,11 @@ abstract class SystemCommandExecution extends DataFlow::Node {
/** Holds if the command execution happens synchronously. */
abstract predicate isSync();
+
+ /**
+ * Gets the data-flow node (if it exists) for an options argument.
+ */
+ abstract DataFlow::Node getOptionsArg();
}
/**
diff --git a/javascript/ql/src/semmle/javascript/frameworks/NodeJSLib.qll b/javascript/ql/src/semmle/javascript/frameworks/NodeJSLib.qll
index 95600e072f66..2eed3f1586b2 100644
--- a/javascript/ql/src/semmle/javascript/frameworks/NodeJSLib.qll
+++ b/javascript/ql/src/semmle/javascript/frameworks/NodeJSLib.qll
@@ -627,6 +627,18 @@ module NodeJSLib {
override predicate isSync() {
"Sync" = methodName.suffix(methodName.length() - 4)
}
+
+ override DataFlow::Node getOptionsArg() {
+ not result.getALocalSource() instanceof DataFlow::FunctionNode and // looks like callback
+ not result.getALocalSource() instanceof DataFlow::ArrayCreationNode and // looks like argumentlist
+ not result = getArgument(0) and
+ // fork/spawn and all sync methos always has options as the last argument
+ if methodName.regexpMatch("fork.*") or methodName.regexpMatch("spawn.*") or methodName.regexpMatch(".*Sync") then
+ result = getLastArgument()
+ else
+ // the rest (exec/execFile) has the options argument as their second last.
+ result = getArgument(this.getNumArgument() - 2)
+ }
}
/**
diff --git a/javascript/ql/src/semmle/javascript/frameworks/ShellJS.qll b/javascript/ql/src/semmle/javascript/frameworks/ShellJS.qll
index e459fe9089c7..6944b7e74a38 100644
--- a/javascript/ql/src/semmle/javascript/frameworks/ShellJS.qll
+++ b/javascript/ql/src/semmle/javascript/frameworks/ShellJS.qll
@@ -162,6 +162,13 @@ module ShellJS {
override predicate isShellInterpreted(DataFlow::Node arg) { arg = getACommandArgument() }
override predicate isSync() {none ()}
+
+ override DataFlow::Node getOptionsArg() {
+ result = getLastArgument() and
+ not result = getArgument(0) and
+ not result.getALocalSource() instanceof DataFlow::FunctionNode and // looks like callback
+ not result.getALocalSource() instanceof DataFlow::ArrayCreationNode // looks like argumentlist
+ }
}
/**
diff --git a/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll b/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll
index e0664f93dd1a..466ed29ae0a2 100644
--- a/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll
+++ b/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll
@@ -7,6 +7,7 @@ import javascript
private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::InvokeNode {
int cmdArg;
+ int optionsArg;
boolean shell;
boolean sync;
@@ -14,9 +15,9 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
SystemCommandExecutors() {
exists(string mod, DataFlow::SourceNode callee |
exists(string method |
- mod = "cross-spawn" and method = "sync" and cmdArg = 0 and shell = false
+ mod = "cross-spawn" and method = "sync" and cmdArg = 0 and shell = false and optionsArg = -1
or
- mod = "execa" and
+ mod = "execa" and optionsArg = -1 and
(
shell = false and
(
@@ -40,21 +41,21 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
(
shell = false and
(
- mod = "cross-spawn" and cmdArg = 0
+ mod = "cross-spawn" and cmdArg = 0 and optionsArg = -1
or
- mod = "cross-spawn-async" and cmdArg = 0
+ mod = "cross-spawn-async" and cmdArg = 0 and optionsArg = -1
or
- mod = "exec-async" and cmdArg = 0
+ mod = "exec-async" and cmdArg = 0 and optionsArg = -1
or
- mod = "execa" and cmdArg = 0
+ mod = "execa" and cmdArg = 0 and optionsArg = -1
)
or
shell = true and
(
- mod = "exec" and
+ mod = "exec" and optionsArg = -2 and
cmdArg = 0
or
- mod = "remote-exec" and cmdArg = 1
+ mod = "remote-exec" and cmdArg = 1 and optionsArg = -1
)
) and
callee = DataFlow::moduleImport(mod)
@@ -69,8 +70,16 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
arg = getACommandArgument() and shell = true
}
- override predicate isSync() {
- sync = true
+ override predicate isSync() { sync = true }
+
+ override DataFlow::Node getOptionsArg() {
+ (if optionsArg < 0 then
+ result = getArgument(getNumArgument() - optionsArg)
+ else
+ result = getArgument(optionsArg)) and
+ not result = getArgument(0) and
+ not result.getALocalSource() instanceof DataFlow::FunctionNode and // looks like callback
+ not result.getALocalSource() instanceof DataFlow::ArrayCreationNode // looks like argumentlist
}
}
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index cccf7937c558..b40c0cc3a893 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -41,21 +41,7 @@ private class CommandCall extends DataFlow::InvokeNode {
* Gets the data-flow node (if it exists) for an options argument for an `exec`-like call.
*/
DataFlow::Node getOptionsArg() {
- exists(int n |
- n >= 1 and
- // if there is a command-list, then the options is at least the third argument.
- (not exists(command.getArgumentList()) or n >= 2) and
- // async exec calls can have a callback as their last call.
- if command.isSync() or not exists(getCallback())
- then n < getNumArgument()
- else n < getNumArgument() - 1
- |
- result = getArgument(n)
- )
- or
- // Fallback in case normal API conventions are broken.
- result = getAnArgument() and
- result.getALocalSource() instanceof DataFlow::ObjectLiteralNode
+ result = command.getOptionsArg()
}
/**
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
index c0de47ed9957..f78680f30edb 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
@@ -71,6 +71,23 @@ syncCommand
| uselesscat.js:100:1:100:56 | execFil ... ptions) |
| uselesscat.js:104:1:104:31 | execFil ... cat` ]) |
| uselesscat.js:136:17:138:2 | execSyn ... tf8'\\n}) |
+options
+| child_process-test.js:53:5:53:59 | cp.spaw ... cmd])) | child_process-test.js:53:25:53:58 | ['/C', ... , cmd]) |
+| child_process-test.js:54:5:54:50 | cp.spaw ... t(cmd)) | child_process-test.js:54:25:54:49 | ['/C', ... at(cmd) |
+| child_process-test.js:64:3:64:21 | cp.spawn(cmd, args) | child_process-test.js:64:17:64:20 | args |
+| uselesscat.js:28:1:28:39 | execSyn ... 1000}) | uselesscat.js:28:28:28:38 | {uid: 1000} |
+| uselesscat.js:30:1:30:64 | exec('c ... t) { }) | uselesscat.js:30:26:30:38 | { cwd: './' } |
+| uselesscat.js:34:1:34:54 | execSyn ... utf8'}) | uselesscat.js:34:36:34:53 | {encoding: 'utf8'} |
+| uselesscat.js:36:1:36:77 | execSyn ... utf8'}) | uselesscat.js:36:36:36:76 | { uid: ... 'utf8'} |
+| uselesscat.js:69:1:72:2 | execFil ... ut);\\n}) | uselesscat.js:69:38:69:55 | {encoding: 'utf8'} |
+| uselesscat.js:74:1:74:60 | execFil ... utf8'}) | uselesscat.js:74:42:74:59 | {encoding: 'utf8'} |
+| uselesscat.js:79:1:79:46 | execFil ... opts) | uselesscat.js:79:42:79:45 | opts |
+| uselesscat.js:82:1:82:90 | execFil ... String) | uselesscat.js:82:42:82:89 | anOptsF ... oString |
+| uselesscat.js:84:1:84:115 | execFil ... ring'}) | uselesscat.js:84:42:84:114 | {encodi ... tring'} |
+| uselesscat.js:86:1:86:75 | execFil ... utf8'}) | uselesscat.js:86:57:86:74 | {encoding: 'utf8'} |
+| uselesscat.js:100:1:100:56 | execFil ... ptions) | uselesscat.js:100:42:100:55 | unknownOptions |
+| uselesscat.js:111:1:111:51 | spawn(' ... it'] }) | uselesscat.js:111:14:111:50 | { stdio ... rit'] } |
+| uselesscat.js:136:17:138:2 | execSyn ... tf8'\\n}) | uselesscat.js:136:51:138:1 | { // NO ... utf8'\\n} |
#select
| False negative | uselesscat.js:54:42:54:69 | // NOT ... lagged] |
| False positive | uselesscat.js:44:37:44:85 | // OK [ ... le read |
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql
index a287a57ff551..a70f53016bda 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql
@@ -21,4 +21,8 @@ query string readFile(UselessCat cat) { result = PrettyPrintCatCall::createReadF
query SystemCommandExecution syncCommand() {
result.isSync()
+}
+
+query DataFlow::Node options(SystemCommandExecution sys) {
+ result = sys.getOptionsArg()
}
\ No newline at end of file
From a768e937f0c64052881f4c4ef389507dca5ac0e8 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Mon, 24 Feb 2020 13:08:50 +0100
Subject: [PATCH 19/37] complete qldoc
---
.../ql/src/semmle/javascript/security/UselessUseOfCat.qll | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index b40c0cc3a893..80a0eb6398d1 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -78,7 +78,7 @@ private class CommandCall extends DataFlow::InvokeNode {
/**
* Gets the constant string parts from a data-flow node.
- * Either the string is some constant
+ * Either the result is a constant string value that the node can hold, or the node is a string-concatenation and the result is the string parts from the concatenation.
*/
private string getConstantStringParts(DataFlow::Node node) {
node.mayHaveStringValue(result)
From 051de247b0fa54119453e36b348a870d696c9cb5 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Mon, 24 Feb 2020 13:11:30 +0100
Subject: [PATCH 20/37] change regexpMatch to regexpFind
---
.../ql/src/semmle/javascript/security/UselessUseOfCat.qll | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index 80a0eb6398d1..a19fce9027f3 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -102,7 +102,7 @@ class UselessCat extends CommandCall {
)
) and
// wildcards, pipes, redirections, other bash features, and multiple files (spaces) are OK.
- not getNonCommandConstantString().regexpMatch(".*(\\*|\\||>|<| |\\$|&|,|\\`).*") and
+ not exists(getNonCommandConstantString().regexpFind("\\*|\\||>|<| |\\$|&|,|\\`", _, _)) and
// Only acceptable option is "encoding", everything else is non-trivial to emulate with fs.readFile.
(
not exists(getOptionsArg())
From fb94af976414aec823514f8d1ee3e1abbf507e1d Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Mon, 24 Feb 2020 13:18:15 +0100
Subject: [PATCH 21/37] remove the last dependency on PrettyPrinting
---
.../semmle/javascript/security/UselessUseOfCat.qll | 11 ++++-------
.../Security/CWE-078/UselessUseOfCat.expected | 2 +-
.../test/query-tests/Security/CWE-078/uselesscat.js | 8 ++++++--
3 files changed, 11 insertions(+), 10 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index a19fce9027f3..bcdaa2eff4c3 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -93,13 +93,10 @@ class UselessCat extends CommandCall {
UselessCat() {
this = command and
isACallTo(getACatExecuteable()) and
- // There is a file to read, and not just a pair of quotes.
- (
- not exists(PrettyPrintCatCall::createFileArgument(this))
- or
- exists(string fileArg | fileArg = PrettyPrintCatCall::createFileArgument(this) |
- fileArg.length() >= 3
- )
+ // There is a file to read, it's not just spawning `cat`.
+ not (
+ not exists(getArgumentList()) and
+ getArgument(0).mayHaveStringValue(getACatExecuteable())
) and
// wildcards, pipes, redirections, other bash features, and multiple files (spaces) are OK.
not exists(getNonCommandConstantString().regexpFind("\\*|\\||>|<| |\\$|&|,|\\`", _, _)) and
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
index f78680f30edb..9e53b8cc422a 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
@@ -45,7 +45,7 @@ syncCommand
| tst_shell-command-injection-from-environment.js:5:2:5:54 | cp.exec ... temp")) |
| uselesscat.js:16:1:16:29 | execSyn ... uinfo') |
| uselesscat.js:18:1:18:26 | execSyn ... path}`) |
-| uselesscat.js:20:1:20:50 | child_p ... wc -l') |
+| uselesscat.js:20:1:20:36 | execSyn ... wc -l') |
| uselesscat.js:22:1:22:38 | execSyn ... o/bar') |
| uselesscat.js:24:1:24:35 | execSyn ... o/bar`) |
| uselesscat.js:28:1:28:39 | execSyn ... 1000}) |
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
index c50e0fc1f5ee..86647b9fc296 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
+++ b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
@@ -17,7 +17,7 @@ execSync('cat /proc/cpuinfo').toString(); // NOT OK.
execSync(`cat ${newpath}`) // NOT OK
-child_process.execSync('cat package.json | wc -l'); // OK - pipes!
+execSync('cat package.json | wc -l'); // OK - pipes!
execSync('cat /proc/cpuinfo /foo/bar').toString(); // OK multiple files.
@@ -135,4 +135,8 @@ console.log(notDead);
const stdout2 = execSync('cat /etc/dnsmasq.conf', { // NOT OK.
encoding: 'utf8'
-});
\ No newline at end of file
+});
+
+exec('/bin/cat', function (e, s) {}); // OK
+
+spawn("cat") // OK
\ No newline at end of file
From a779ae58a88b3307eec6dd9e26b7bc27a7476c22 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Mon, 24 Feb 2020 14:03:41 +0100
Subject: [PATCH 22/37] add qhelp
---
.../Security/CWE-078/UselessUseOfCat.qhelp | 47 +++++++++++++------
.../CWE-078/examples/useless-cat-fixed.js | 5 ++
.../Security/CWE-078/examples/useless-cat.js | 5 ++
3 files changed, 42 insertions(+), 15 deletions(-)
create mode 100644 javascript/ql/src/Security/CWE-078/examples/useless-cat-fixed.js
create mode 100644 javascript/ql/src/Security/CWE-078/examples/useless-cat.js
diff --git a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
index 452f630712d2..85a7a2e49494 100644
--- a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
+++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
@@ -3,28 +3,45 @@
"qhelp.dtd">
-
- Useless use of cat
-
-
-
+Using the unix command cat to simply read a file is a
+unnecessarily complex way to achieve something that can be done simpler and
+safer using the Node.js fs.readFile API.
+
+
+The use of cat for simple file reads leads to code that is
+unportable, inefficient, complex, and can lead to subtle bugs or even
+security vulnerabilities.
+
-
- TODO: This is a placeholder
-
-
+
+Use fs.readFile or fs.readFileSync to read files
+from the file system.
+
-
-
+
+The following example shows code that reads a file using cat:
+
+
+
+The code in the example will break if the input name contain
+special characters (including space), the code does not work on windows,
+and if the input is user controlled a command injection attack can happen.
+
+To avoid these potential issues the fs.readFile API can be
+used instead:
+
+
+
-
- OWASP:
- Command Injection.
-
+
+OWASP: Command Injection.
+Node.js: File System API.
+
+
diff --git a/javascript/ql/src/Security/CWE-078/examples/useless-cat-fixed.js b/javascript/ql/src/Security/CWE-078/examples/useless-cat-fixed.js
new file mode 100644
index 000000000000..225fa1f58699
--- /dev/null
+++ b/javascript/ql/src/Security/CWE-078/examples/useless-cat-fixed.js
@@ -0,0 +1,5 @@
+var fs = require('fs');
+
+module.exports = function (name) {
+ return fs.readFileSync(name).toString();
+};
diff --git a/javascript/ql/src/Security/CWE-078/examples/useless-cat.js b/javascript/ql/src/Security/CWE-078/examples/useless-cat.js
new file mode 100644
index 000000000000..78f099d0e4c2
--- /dev/null
+++ b/javascript/ql/src/Security/CWE-078/examples/useless-cat.js
@@ -0,0 +1,5 @@
+var child_process = require('child_process');
+
+module.exports = function (name) {
+ return child_process.execSync("cat " + name).toString();
+};
From b72404dc9990c56896e1ae06707d4b20ba519aa6 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Mon, 24 Feb 2020 14:07:49 +0100
Subject: [PATCH 23/37] add change note
---
change-notes/1.24/analysis-javascript.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/change-notes/1.24/analysis-javascript.md b/change-notes/1.24/analysis-javascript.md
index 47fb2dc590be..eaebf0c12510 100644
--- a/change-notes/1.24/analysis-javascript.md
+++ b/change-notes/1.24/analysis-javascript.md
@@ -39,6 +39,8 @@
| Missing await (`js/missing-await`) | correctness | Highlights expressions that operate directly on a promise object in a nonsensical way, instead of awaiting its result. Results are shown on LGTM by default. |
| Prototype pollution in utility function (`js/prototype-pollution-utility`) | security, external/cwe/cwe-400, external/cwe/cwe-471 | Highlights recursive copying operations that are susceptible to prototype pollution. Results are shown on LGTM by default. |
| Unsafe jQuery plugin (`js/unsafe-jquery-plugin`) | Highlights potential XSS vulnerabilities in unsafely designed jQuery plugins. Results are shown on LGTM by default. |
+| Useless use of cat (`js/useless-use-of-cat`) | correctness, security, maintainability | Highlights command executions of cat where the fs API should be used instead. Results are shown on LGTM by default. |
+
## Changes to existing queries
From b20e8520f6cfe15fe5217daeaaea37450b987817 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Mon, 24 Feb 2020 14:52:08 +0100
Subject: [PATCH 24/37] add default message if not pretty printed call can be
created
---
.../ql/src/Security/CWE-078/UselessUseOfCat.ql | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
index 4f080711719b..9f28c5fb0cf4 100644
--- a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
+++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
@@ -14,10 +14,12 @@ import javascript
import semmle.javascript.security.UselessUseOfCat
import semmle.javascript.RestrictedLocations
-
from UselessCat cat, string message
-where
- message = " Can be replaced with: " + PrettyPrintCatCall::createReadFileCall(cat)
- or
- not exists(PrettyPrintCatCall::createReadFileCall(cat)) and message = ""
+where
+ message = " Can be replaced with: " + PrettyPrintCatCall::createReadFileCall(cat)
+ or
+ not exists(PrettyPrintCatCall::createReadFileCall(cat)) and
+ if cat.isSync()
+ then message = " Can be replaced with a call to fs.readFileSync(..)."
+ else message = " Can be replaced with a call to fs.readFile(..)."
select cat.asExpr().(FirstLineOf), "Useless use of `cat`." + message
\ No newline at end of file
From afd6ea262840811512b0b5a7fe62b3adbc342d9c Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Mon, 24 Feb 2020 17:54:29 +0100
Subject: [PATCH 25/37] small correction in doc + autoformat
---
.../ql/src/semmle/javascript/security/UselessUseOfCat.qll | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index bcdaa2eff4c3..29b18ecd2c94 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -40,9 +40,7 @@ private class CommandCall extends DataFlow::InvokeNode {
/**
* Gets the data-flow node (if it exists) for an options argument for an `exec`-like call.
*/
- DataFlow::Node getOptionsArg() {
- result = command.getOptionsArg()
- }
+ DataFlow::Node getOptionsArg() { result = command.getOptionsArg() }
/**
* Gets the constant-string parts that are not part of the command itself.
@@ -78,7 +76,7 @@ private class CommandCall extends DataFlow::InvokeNode {
/**
* Gets the constant string parts from a data-flow node.
- * Either the result is a constant string value that the node can hold, or the node is a string-concatenation and the result is the string parts from the concatenation.
+ * Either the result is a constant string value that the node can hold, or the node is a string-concatenation and the result is the string parts from the concatenation.
*/
private string getConstantStringParts(DataFlow::Node node) {
node.mayHaveStringValue(result)
@@ -108,7 +106,7 @@ class UselessCat extends CommandCall {
prop = "encoding"
)
) and
- // If there is a callback, then it must either have one or two arguments, or if there is a third argument it must be unused.
+ // If there is a callback, then it must either have one or two parameters, or if there is a third parameter it must be unused.
(
not exists(getCallback())
or
From d540caecdddc3b6bff300f5be6f2691394c954df Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Tue, 25 Feb 2020 10:04:51 +0100
Subject: [PATCH 26/37] Apply suggestions from code review
Co-Authored-By: Esben Sparre Andreasen
---
.../ql/src/semmle/javascript/security/UselessUseOfCat.qll | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index 29b18ecd2c94..1d6480f3b094 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -97,7 +97,7 @@ class UselessCat extends CommandCall {
getArgument(0).mayHaveStringValue(getACatExecuteable())
) and
// wildcards, pipes, redirections, other bash features, and multiple files (spaces) are OK.
- not exists(getNonCommandConstantString().regexpFind("\\*|\\||>|<| |\\$|&|,|\\`", _, _)) and
+ not exists(getNonCommandConstantString().regexpFind("\\*|\\||>|<| |\\$|&|,|\\`| ", _, _)) and
// Only acceptable option is "encoding", everything else is non-trivial to emulate with fs.readFile.
(
not exists(getOptionsArg())
@@ -135,7 +135,7 @@ class UselessCat extends CommandCall {
* Gets a string used to call `cat`.
*/
string getACatExecuteable() {
- result = "cat" or result = "/bin/cat" or result = "sudo cat" or result = "sudo /bin/cat"
+ result = "cat" or result = "/bin/cat"
}
/**
From 87d283aa6c7089e1869c367c1c3d9941a326f976 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Tue, 25 Feb 2020 10:50:59 +0100
Subject: [PATCH 27/37] add tests for third party command execution libraries
(and two small fixes)
---
.../frameworks/SystemCommandExecutors.qll | 5 ++--
.../javascript/security/UselessUseOfCat.qll | 6 ++---
.../Security/CWE-078/UselessUseOfCat.expected | 19 ++++++++++++++
.../Security/CWE-078/uselesscat.js | 26 ++++++++++++++++++-
4 files changed, 50 insertions(+), 6 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll b/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll
index 466ed29ae0a2..89a0929a182b 100644
--- a/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll
+++ b/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll
@@ -70,14 +70,15 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
arg = getACommandArgument() and shell = true
}
+ override DataFlow::Node getArgumentList() { shell = false and result = getArgument(1) }
+
override predicate isSync() { sync = true }
override DataFlow::Node getOptionsArg() {
(if optionsArg < 0 then
- result = getArgument(getNumArgument() - optionsArg)
+ result = getArgument(getNumArgument() + optionsArg) and getNumArgument() + optionsArg > cmdArg
else
result = getArgument(optionsArg)) and
- not result = getArgument(0) and
not result.getALocalSource() instanceof DataFlow::FunctionNode and // looks like callback
not result.getALocalSource() instanceof DataFlow::ArrayCreationNode // looks like argumentlist
}
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index 1d6480f3b094..7cd66f7981b9 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -21,9 +21,9 @@ private class CommandCall extends DataFlow::InvokeNode {
predicate isSync() { command.isSync() }
/**
- * Gets an argument to this command execution that specifies the argument list to the command.
+ * Gets a list that specifies the arguments given to the command.
*/
- DataFlow::Node getArgumentList() { result = command.getArgumentList() }
+ DataFlow::ArrayCreationNode getArgumentList() { result = command.getArgumentList().getALocalSource() }
/**
* Gets the callback (if it exists) for an async `exec`-like call.
@@ -35,7 +35,7 @@ private class CommandCall extends DataFlow::InvokeNode {
/**
* Holds if the executed command execution has an argument list as a separate argument.
*/
- predicate hasArgumentList() { exists(command.getArgumentList()) }
+ predicate hasArgumentList() { exists(getArgumentList()) }
/**
* Gets the data-flow node (if it exists) for an options argument for an `exec`-like call.
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
index 9e53b8cc422a..4f2dae3d7b1a 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected
@@ -22,6 +22,18 @@ readFile
| uselesscat.js:121:12:121:64 | exec("c ... (out)}) | fs.readFile("foo/bar", (err, out) => {...}) |
| uselesscat.js:127:14:127:66 | exec("c ... (out)}) | fs.readFile("foo/bar", (err, out) => {...}) |
| uselesscat.js:136:17:138:2 | execSyn ... tf8'\\n}) | fs.readFileSync("/etc/dnsmasq.conf", ...) |
+| uselesscat.js:146:1:146:61 | shelljs ... (out)}) | fs.readFile("foo/bar", (err, out) => {...}) |
+| uselesscat.js:147:1:147:47 | shelljs ... utf8'}) | fs.readFile("foo/bar", {encoding: 'utf8'}) |
+| uselesscat.js:148:1:148:81 | shelljs ... (out)}) | fs.readFile("foo/bar", (err, out) => {...}) |
+| uselesscat.js:151:1:151:48 | cspawn( ... tf8' }) | fs.readFile("foo/bar", { encoding: 'utf8' }) |
+| uselesscat.js:152:1:152:82 | cspawn( ... (out)}) | fs.readFile("foo/bar", (err, out) => {...}) |
+| uselesscat.js:153:1:153:60 | cspawn( ... (out)}) | fs.readFile("foo/bar", (err, out) => {...}) |
+| uselesscat.js:154:1:154:26 | cspawn( ... /bar']) | fs.readFile("foo/bar") |
+| uselesscat.js:158:16:158:46 | cspawn. ... /bar']) | fs.readFileSync("foo/bar") |
+| uselesscat.js:159:16:159:68 | cspawn. ... tf8' }) | fs.readFileSync("foo/bar", { encoding: 'utf8' }) |
+| uselesscat.js:162:1:162:56 | execmod ... (out)}) | fs.readFile("foo/bar", (err, out) => {...}) |
+| uselesscat.js:163:1:163:42 | execmod ... utf8'}) | fs.readFile("foo/bar") |
+| uselesscat.js:164:1:164:76 | execmod ... (out)}) | fs.readFile("foo/bar", {encoding: 'utf8'}, (err, out) => {...}) |
syncCommand
| child_process-test.js:9:5:9:22 | cp.execSync("foo") |
| child_process-test.js:11:5:11:26 | cp.exec ... ("foo") |
@@ -71,6 +83,8 @@ syncCommand
| uselesscat.js:100:1:100:56 | execFil ... ptions) |
| uselesscat.js:104:1:104:31 | execFil ... cat` ]) |
| uselesscat.js:136:17:138:2 | execSyn ... tf8'\\n}) |
+| uselesscat.js:158:16:158:46 | cspawn. ... /bar']) |
+| uselesscat.js:159:16:159:68 | cspawn. ... tf8' }) |
options
| child_process-test.js:53:5:53:59 | cp.spaw ... cmd])) | child_process-test.js:53:25:53:58 | ['/C', ... , cmd]) |
| child_process-test.js:54:5:54:50 | cp.spaw ... t(cmd)) | child_process-test.js:54:25:54:49 | ['/C', ... at(cmd) |
@@ -88,6 +102,11 @@ options
| uselesscat.js:100:1:100:56 | execFil ... ptions) | uselesscat.js:100:42:100:55 | unknownOptions |
| uselesscat.js:111:1:111:51 | spawn(' ... it'] }) | uselesscat.js:111:14:111:50 | { stdio ... rit'] } |
| uselesscat.js:136:17:138:2 | execSyn ... tf8'\\n}) | uselesscat.js:136:51:138:1 | { // NO ... utf8'\\n} |
+| uselesscat.js:147:1:147:47 | shelljs ... utf8'}) | uselesscat.js:147:29:147:46 | {encoding: 'utf8'} |
+| uselesscat.js:151:1:151:48 | cspawn( ... tf8' }) | uselesscat.js:151:28:151:47 | { encoding: 'utf8' } |
+| uselesscat.js:156:1:156:35 | cspawn( ... tf8' }) | uselesscat.js:156:15:156:34 | { encoding: 'utf8' } |
+| uselesscat.js:159:16:159:68 | cspawn. ... tf8' }) | uselesscat.js:159:48:159:67 | { encoding: 'utf8' } |
+| uselesscat.js:164:1:164:76 | execmod ... (out)}) | uselesscat.js:164:24:164:41 | {encoding: 'utf8'} |
#select
| False negative | uselesscat.js:54:42:54:69 | // NOT ... lagged] |
| False positive | uselesscat.js:44:37:44:85 | // OK [ ... le read |
diff --git a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
index 86647b9fc296..ace4784b3806 100644
--- a/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
+++ b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js
@@ -139,4 +139,28 @@ const stdout2 = execSync('cat /etc/dnsmasq.conf', { // NOT OK.
exec('/bin/cat', function (e, s) {}); // OK
-spawn("cat") // OK
\ No newline at end of file
+spawn("cat") // OK
+
+
+var shelljs = require("shelljs");
+shelljs.exec("cat foo/bar", (err, out) => {console.log(out)}); // NOT OK
+shelljs.exec("cat foo/bar", {encoding: 'utf8'}); // NOT OK
+shelljs.exec("cat foo/bar", {encoding: 'utf8'}, (err, out) => {console.log(out)}); // NOT OK
+
+let cspawn = require('cross-spawn');
+cspawn('cat', ['foo/bar'], { encoding: 'utf8' }); // NOT OK
+cspawn('cat', ['foo/bar'], { encoding: 'utf8' }, (err, out) => {console.log(out)}); // NOT OK
+cspawn('cat', ['foo/bar'], (err, out) => {console.log(out)}); // NOT OK
+cspawn('cat', ['foo/bar']); // NOT OK
+cspawn('cat', (err, out) => {console.log(out)}); // OK
+cspawn('cat', { encoding: 'utf8' }); // OK
+
+let myResult = cspawn.sync('cat', ['foo/bar']); // NOT OK
+let myResult = cspawn.sync('cat', ['foo/bar'], { encoding: 'utf8' }); // NOT OK
+
+var execmod = require('exec');
+execmod("cat foo/bar", (err, out) => {console.log(out)}); // NOT OK
+execmod("cat foo/bar", {encoding: 'utf8'}); // NOT OK
+execmod("cat foo/bar", {encoding: 'utf8'}, (err, out) => {console.log(out)}); // NOT OK
+
+
\ No newline at end of file
From 8d26f3219950f887ab3ccb27e7807e94deb17fce Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Tue, 25 Feb 2020 10:53:07 +0100
Subject: [PATCH 28/37] arg -> param
---
.../src/semmle/javascript/security/UselessUseOfCat.qll | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index 7cd66f7981b9..674aa83e52b9 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -185,20 +185,20 @@ module PrettyPrintCatCall {
* Create a string representing the callback `func`.
*/
string createCallbackString(DataFlow::FunctionNode func) {
- exists(string args | args = createCallbackArgs(func) |
+ exists(string params | params = createCallbackParams(func) |
if func.getFunction() instanceof ArrowFunctionExpr
then
if func.getFunction().getBody() instanceof Expr
- then result = ", (" + args + ") => ..."
- else result = ", (" + args + ") => {...}"
- else result = ", function(" + args + ") {...}"
+ then result = ", (" + params + ") => ..."
+ else result = ", (" + params + ") => {...}"
+ else result = ", function(" + params + ") {...}"
)
}
/**
* Create a string concatenation of the parameter names in a function `func`.
*/
- private string createCallbackArgs(DataFlow::FunctionNode func) {
+ private string createCallbackParams(DataFlow::FunctionNode func) {
result =
concat(int i |
i = [0 .. func.getNumParameter()]
From c83c27cbc4bbc4d4f8f4d1e999625449534ab071 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Tue, 25 Feb 2020 11:11:58 +0100
Subject: [PATCH 29/37] add extra sanity-check that the output looks good
---
.../javascript/security/UselessUseOfCat.qll | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index 674aa83e52b9..a0dfaa26fb62 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -74,6 +74,14 @@ private class CommandCall extends DataFlow::InvokeNode {
}
}
+/**
+ * Holds if the input `str` contains some character that might be interpreted in a non-trivial way by a shell.
+ */
+bindingset[str]
+predicate containsNonTrivialBashChar(string str) {
+ exists(str.regexpFind("\\*|\\||>|<| |\\$|&|,|\\`| ", _, _))
+}
+
/**
* Gets the constant string parts from a data-flow node.
* Either the result is a constant string value that the node can hold, or the node is a string-concatenation and the result is the string parts from the concatenation.
@@ -97,7 +105,7 @@ class UselessCat extends CommandCall {
getArgument(0).mayHaveStringValue(getACatExecuteable())
) and
// wildcards, pipes, redirections, other bash features, and multiple files (spaces) are OK.
- not exists(getNonCommandConstantString().regexpFind("\\*|\\||>|<| |\\$|&|,|\\`| ", _, _)) and
+ not containsNonTrivialBashChar(getNonCommandConstantString()) and
// Only acceptable option is "encoding", everything else is non-trivial to emulate with fs.readFile.
(
not exists(getOptionsArg())
@@ -146,7 +154,7 @@ module PrettyPrintCatCall {
* Create a string representation of an equivalent call to `fs.readFile` for a given command execution `cat`.
*/
string createReadFileCall(UselessCat cat) {
- exists(string sync, string extraArg, string callback |
+ exists(string sync, string extraArg, string callback, string fileArg |
(if cat.isSync() then sync = "Sync" else sync = "") and
(
exists(cat.getOptionsArg()) and
@@ -163,10 +171,12 @@ module PrettyPrintCatCall {
callback = createCallbackString(cat.getCallback())
or
callback = "" and not exists(cat.getCallback())
- )
+ ) and
+ fileArg = createFileArgument(cat).trim() and
+ not(containsNonTrivialBashChar(fileArg.regexpReplaceAll("\\$|\\`| ", ""))) // string concat might contain " ", template strings might contain "$" or `, and that is OK.
|
result =
- "fs.readFile" + sync + "(" + createFileArgument(cat).trim() + extraArg + callback + ")"
+ "fs.readFile" + sync + "(" + fileArg + extraArg + callback + ")"
)
}
From bb911bbbf132e3c58f46d70c1ee34eedf6d6ee23 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Thu, 27 Feb 2020 12:38:06 +0100
Subject: [PATCH 30/37] Apply suggestions from code review
Co-Authored-By: Esben Sparre Andreasen
---
.../ql/src/semmle/javascript/security/UselessUseOfCat.qll | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index a0dfaa26fb62..c154885925f8 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -78,8 +78,8 @@ private class CommandCall extends DataFlow::InvokeNode {
* Holds if the input `str` contains some character that might be interpreted in a non-trivial way by a shell.
*/
bindingset[str]
-predicate containsNonTrivialBashChar(string str) {
- exists(str.regexpFind("\\*|\\||>|<| |\\$|&|,|\\`| ", _, _))
+private predicate containsNonTrivialShellChar(string str) {
+ exists(str.regexpFind("\\*|\\||>|<| |\\$|&|,|\\`| |;", _, _))
}
/**
@@ -142,7 +142,7 @@ class UselessCat extends CommandCall {
/**
* Gets a string used to call `cat`.
*/
-string getACatExecuteable() {
+private string getACatExecuteable() {
result = "cat" or result = "/bin/cat"
}
@@ -173,6 +173,7 @@ module PrettyPrintCatCall {
callback = "" and not exists(cat.getCallback())
) and
fileArg = createFileArgument(cat).trim() and
+ // sanity check in case of surprising `toString` results, other uses of `containsNonTrivialBashChar` should ensure that this conjunct will hold most of the time
not(containsNonTrivialBashChar(fileArg.regexpReplaceAll("\\$|\\`| ", ""))) // string concat might contain " ", template strings might contain "$" or `, and that is OK.
|
result =
From a872d7c5c52ef4b5d2d1cc1e23bc046f1feb7b65 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Thu, 27 Feb 2020 12:42:22 +0100
Subject: [PATCH 31/37] add comment about negative optionsArg
---
.../src/semmle/javascript/frameworks/SystemCommandExecutors.qll | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll b/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll
index 89a0929a182b..dbb398be9537 100644
--- a/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll
+++ b/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll
@@ -7,7 +7,7 @@ import javascript
private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::InvokeNode {
int cmdArg;
- int optionsArg;
+ int optionsArg; // either a positive number representing the n'th argument, or a negative number representing the n'th last argument (e.g. -2 is the second last argument).
boolean shell;
boolean sync;
From 17f1974e05a6719dd0587aebd6152ec18402451a Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Fri, 28 Feb 2020 09:43:32 +0100
Subject: [PATCH 32/37] Apply suggestions from code review
Co-Authored-By: mc <42146119+mchammer01@users.noreply.github.com>
---
change-notes/1.24/analysis-javascript.md | 2 +-
.../ql/src/Security/CWE-078/UselessUseOfCat.qhelp | 15 +++++++--------
.../ql/src/Security/CWE-078/UselessUseOfCat.ql | 4 ++--
3 files changed, 10 insertions(+), 11 deletions(-)
diff --git a/change-notes/1.24/analysis-javascript.md b/change-notes/1.24/analysis-javascript.md
index eaebf0c12510..9c1afc25c07a 100644
--- a/change-notes/1.24/analysis-javascript.md
+++ b/change-notes/1.24/analysis-javascript.md
@@ -39,7 +39,7 @@
| Missing await (`js/missing-await`) | correctness | Highlights expressions that operate directly on a promise object in a nonsensical way, instead of awaiting its result. Results are shown on LGTM by default. |
| Prototype pollution in utility function (`js/prototype-pollution-utility`) | security, external/cwe/cwe-400, external/cwe/cwe-471 | Highlights recursive copying operations that are susceptible to prototype pollution. Results are shown on LGTM by default. |
| Unsafe jQuery plugin (`js/unsafe-jquery-plugin`) | Highlights potential XSS vulnerabilities in unsafely designed jQuery plugins. Results are shown on LGTM by default. |
-| Useless use of cat (`js/useless-use-of-cat`) | correctness, security, maintainability | Highlights command executions of cat where the fs API should be used instead. Results are shown on LGTM by default. |
+| Useless use of cat (`js/useless-use-of-cat`) | correctness, security, maintainability | Highlights command executions of `cat` where the fs API should be used instead. Results are shown on LGTM by default. |
## Changes to existing queries
diff --git a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
index 85a7a2e49494..51ac109ae6cd 100644
--- a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
+++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
@@ -3,9 +3,9 @@
"qhelp.dtd">
-Using the unix command cat to simply read a file is a
-unnecessarily complex way to achieve something that can be done simpler and
-safer using the Node.js fs.readFile API.
+
Using the unix command cat only to read a file is a
+an unnecessarily complex way to achieve something that can be done in a simpler and
+safer manner using the Node.js fs.readFile API.
The use of cat for simple file reads leads to code that is
@@ -25,12 +25,11 @@ from the file system.
-
The code in the example will break if the input name contain
-special characters (including space), the code does not work on windows,
-and if the input is user controlled a command injection attack can happen.
+The code in the example will break if the input name contains
+special characters (including space). Additionally, it does not work on Windows
+and if the input is user-controlled, a command injection attack can happen.
-To avoid these potential issues the fs.readFile API can be
-used instead:
+The fs.readFile API should be used to avoid these potential issues:
diff --git a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
index 9f28c5fb0cf4..0c33fc8c06f6 100644
--- a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
+++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
@@ -1,6 +1,6 @@
/**
* @name Useless use of cat
- * @description Using `cat`-process to simply read a file is unnecessarily complex, inefficient, unportable, can lead to subtle bugs, or even security vulnerabilities.
+ * @description Using the `cat` process to read a file is unnecessarily complex, inefficient, unportable, and can lead to subtle bugs, or even security vulnerabilities.
* @kind problem
* @problem.severity error
* @precision high
@@ -22,4 +22,4 @@ where
if cat.isSync()
then message = " Can be replaced with a call to fs.readFileSync(..)."
else message = " Can be replaced with a call to fs.readFile(..)."
-select cat.asExpr().(FirstLineOf), "Useless use of `cat`." + message
\ No newline at end of file
+select cat.asExpr().(FirstLineOf), "Useless use of `cat`." + message
From 922779e0491a484d456ba46be45409e798bd50de Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Fri, 28 Feb 2020 09:48:07 +0100
Subject: [PATCH 33/37] remove double a/an and adjust line lenghts
---
javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
index 51ac109ae6cd..8afc06a861e2 100644
--- a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
+++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
@@ -3,9 +3,9 @@
"qhelp.dtd">
-Using the unix command cat only to read a file is a
-an unnecessarily complex way to achieve something that can be done in a simpler and
-safer manner using the Node.js fs.readFile API.
+
Using the unix command cat only to read a file is an
+unnecessarily complex way to achieve something that can be done in a simpler
+and safer manner using the Node.js fs.readFile API.
The use of cat for simple file reads leads to code that is
From d8a96dd7713045de9bc3d7f536d4b5e2fa11a38f Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Fri, 28 Feb 2020 09:55:15 +0100
Subject: [PATCH 34/37] change name to suggestion from previous code review
---
.../ql/src/semmle/javascript/security/UselessUseOfCat.qll | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
index c154885925f8..4c8b38155868 100644
--- a/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
+++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll
@@ -105,7 +105,7 @@ class UselessCat extends CommandCall {
getArgument(0).mayHaveStringValue(getACatExecuteable())
) and
// wildcards, pipes, redirections, other bash features, and multiple files (spaces) are OK.
- not containsNonTrivialBashChar(getNonCommandConstantString()) and
+ not containsNonTrivialShellChar(getNonCommandConstantString()) and
// Only acceptable option is "encoding", everything else is non-trivial to emulate with fs.readFile.
(
not exists(getOptionsArg())
@@ -174,7 +174,7 @@ module PrettyPrintCatCall {
) and
fileArg = createFileArgument(cat).trim() and
// sanity check in case of surprising `toString` results, other uses of `containsNonTrivialBashChar` should ensure that this conjunct will hold most of the time
- not(containsNonTrivialBashChar(fileArg.regexpReplaceAll("\\$|\\`| ", ""))) // string concat might contain " ", template strings might contain "$" or `, and that is OK.
+ not(containsNonTrivialShellChar(fileArg.regexpReplaceAll("\\$|\\`| ", ""))) // string concat might contain " ", template strings might contain "$" or `, and that is OK.
|
result =
"fs.readFile" + sync + "(" + fileArg + extraArg + callback + ")"
From 5e0ae7b4d08f8bce3f5ea9539b80764d93c4da4e Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Fri, 28 Feb 2020 10:23:03 +0100
Subject: [PATCH 35/37] add end
tag
---
javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
index 8afc06a861e2..aa8acaee700e 100644
--- a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
+++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
@@ -29,7 +29,7 @@ from the file system.
special characters (including space). Additionally, it does not work on Windows
and if the input is user-controlled, a command injection attack can happen.
-The fs.readFile API should be used to avoid these potential issues:
+
The fs.readFile API should be used to avoid these potential issues:
From 391b6a833c2927f460526f6d7ce759caade35b14 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Mon, 2 Mar 2020 12:28:51 +0100
Subject: [PATCH 36/37] add link to The Useless Use of Cat Award
---
javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
index aa8acaee700e..5ef218bdf59b 100644
--- a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
+++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp
@@ -36,10 +36,9 @@ and if the input is user-controlled, a command injection attack can happen.
-
-OWASP: Command Injection.
-Node.js: File System API.
-
+OWASP: Command Injection.
+Node.js: File System API.
+The Useless Use of Cat Award.
From 019266e5370a5d5f978900f964543d2a552335f1 Mon Sep 17 00:00:00 2001
From: Erik Krogh Kristensen
Date: Mon, 2 Mar 2020 13:06:08 +0100
Subject: [PATCH 37/37] change name of Useless cat
---
change-notes/1.24/analysis-javascript.md | 2 +-
javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/change-notes/1.24/analysis-javascript.md b/change-notes/1.24/analysis-javascript.md
index abd072711402..dad8caf09bbe 100644
--- a/change-notes/1.24/analysis-javascript.md
+++ b/change-notes/1.24/analysis-javascript.md
@@ -46,7 +46,7 @@
| Polynomial regular expression used on uncontrolled data (`js/polynomial-redos`) | security, external/cwe/cwe-730, external/cwe/cwe-400 | Highlights expensive regular expressions that may be used on malicious input. Results are shown on LGTM by default. |
| Prototype pollution in utility function (`js/prototype-pollution-utility`) | security, external/cwe/cwe-400, external/cwe/cwe-471 | Highlights recursive copying operations that are susceptible to prototype pollution. Results are shown on LGTM by default. |
| Unsafe jQuery plugin (`js/unsafe-jquery-plugin`) | Highlights potential XSS vulnerabilities in unsafely designed jQuery plugins. Results are shown on LGTM by default. |
-| Useless use of cat (`js/useless-use-of-cat`) | correctness, security, maintainability | Highlights command executions of `cat` where the fs API should be used instead. Results are shown on LGTM by default. |
+| Unnecessary use of `cat` process (`js/unnecessary-use-of-cat`) | correctness, security, maintainability | Highlights command executions of `cat` where the fs API should be used instead. Results are shown on LGTM by default. |
## Changes to existing queries
diff --git a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
index 0c33fc8c06f6..6b0ed59e6323 100644
--- a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
+++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql
@@ -1,10 +1,10 @@
/**
- * @name Useless use of cat
+ * @name Unnecessary use of `cat` process
* @description Using the `cat` process to read a file is unnecessarily complex, inefficient, unportable, and can lead to subtle bugs, or even security vulnerabilities.
* @kind problem
* @problem.severity error
* @precision high
- * @id js/useless-use-of-cat
+ * @id js/unnecessary-use-of-cat
* @tags correctness
* security
* maintainability
@@ -22,4 +22,4 @@ where
if cat.isSync()
then message = " Can be replaced with a call to fs.readFileSync(..)."
else message = " Can be replaced with a call to fs.readFile(..)."
-select cat.asExpr().(FirstLineOf), "Useless use of `cat`." + message
+select cat.asExpr().(FirstLineOf), "Unnecessary use of `cat` process." + message