diff --git a/change-notes/1.24/analysis-javascript.md b/change-notes/1.24/analysis-javascript.md index 1cfc11deb480..dad8caf09bbe 100644 --- a/change-notes/1.24/analysis-javascript.md +++ b/change-notes/1.24/analysis-javascript.md @@ -46,6 +46,8 @@ | 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. | +| 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.qhelp b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp new file mode 100644 index 000000000000..5ef218bdf59b --- /dev/null +++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.qhelp @@ -0,0 +1,45 @@ + + + +

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 +unportable, inefficient, complex, and can lead to subtle bugs or even +security vulnerabilities. +

+
+ +

+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 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.

+ +

The fs.readFile API should be used to avoid these potential issues:

+ + + +
+ + +
  • OWASP: Command Injection.
  • +
  • Node.js: File System API.
  • +
  • The Useless Use of Cat Award.
  • + + +
    +
    diff --git a/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql new file mode 100644 index 000000000000..6b0ed59e6323 --- /dev/null +++ b/javascript/ql/src/Security/CWE-078/UselessUseOfCat.ql @@ -0,0 +1,25 @@ +/** + * @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/unnecessary-use-of-cat + * @tags correctness + * security + * maintainability + */ + +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 + 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), "Unnecessary use of `cat` process." + message 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(); +}; diff --git a/javascript/ql/src/semmle/javascript/Concepts.qll b/javascript/ql/src/semmle/javascript/Concepts.qll index 23bbbce3b163..798747af8b8e 100644 --- a/javascript/ql/src/semmle/javascript/Concepts.qll +++ b/javascript/ql/src/semmle/javascript/Concepts.qll @@ -22,6 +22,14 @@ abstract class SystemCommandExecution extends DataFlow::Node { * to the command. */ DataFlow::Node getArgumentList() { none() } + + /** 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 cf2d34844b3b..9a5fe815d556 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/NodeJSLib.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/NodeJSLib.qll @@ -621,6 +621,22 @@ 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) + } + + 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 226054792444..6944b7e74a38 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/ShellJS.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/ShellJS.qll @@ -160,6 +160,15 @@ module ShellJS { override DataFlow::Node getACommandArgument() { result = getArgument(0) } 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 32bfd8f70f7d..8c9535d5f7ef 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll @@ -7,14 +7,17 @@ import javascript private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::InvokeNode { int cmdArg; + 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; 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 + optionsArg = -1 and ( shell = false and ( @@ -30,27 +33,30 @@ 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 ( - 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 + 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) @@ -64,4 +70,30 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I override predicate isShellInterpreted(DataFlow::Node arg) { 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) and + getNumArgument() + optionsArg > cmdArg + else result = getArgument(optionsArg) + ) and + not result.getALocalSource() instanceof DataFlow::FunctionNode and // looks like callback + not result.getALocalSource() instanceof DataFlow::ArrayCreationNode // looks like argumentlist + } +} + +/** + * 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 } 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..4c8b38155868 --- /dev/null +++ b/javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll @@ -0,0 +1,331 @@ +/** + * Provides predicates and classes for working with useless uses of the unix command `cat`. + */ + +import javascript +import Expressions.ExprHasNoEffect +import Declarations.UnusedVariable + +/** + * A call that executes a system command. + * This class provides utility predicates for reasoning about command execution calls. + */ +private class CommandCall extends DataFlow::InvokeNode { + SystemCommandExecution command; + + CommandCall() { this = command } + + /** + * Holds if the call is synchronous (e.g. `execFileSync`). + */ + predicate isSync() { command.isSync() } + + /** + * Gets a list that specifies the arguments given to the command. + */ + DataFlow::ArrayCreationNode getArgumentList() { result = command.getArgumentList().getALocalSource() } + + /** + * 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(getArgumentList()) } + + /** + * Gets the data-flow node (if it exists) for an options argument for an `exec`-like call. + */ + DataFlow::Node getOptionsArg() { result = command.getOptionsArg() } + + /** + * 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 + ) + } +} + +/** + * Holds if the input `str` contains some character that might be interpreted in a non-trivial way by a shell. + */ +bindingset[str] +private predicate containsNonTrivialShellChar(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. + */ +private string getConstantStringParts(DataFlow::Node node) { + node.mayHaveStringValue(result) + or + result = node.(StringOps::ConcatenationRoot).getConstantStringParts() +} + +/** + * A call to a useless use of `cat`. + */ +class UselessCat extends CommandCall { + UselessCat() { + this = command and + isACallTo(getACatExecuteable()) and + // 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 containsNonTrivialShellChar(getNonCommandConstantString()) and + // Only acceptable option is "encoding", everything else is non-trivial to emulate with fs.readFile. + ( + not exists(getOptionsArg()) + or + forex(string prop | exists(getOptionsArg().getALocalSource().getAPropertyWrite(prop)) | + prop = "encoding" + ) + ) and + // 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 + 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())) + ) + ) and + // The process returned by an async call is unused. + ( + isSync() + or + inVoidContext(this.getEnclosingExpr()) + or + this.getEnclosingExpr() = any(UnusedLocal v).getAnAssignedExpr() + ) + } +} + +/** + * Gets a string used to call `cat`. + */ +private string getACatExecuteable() { + result = "cat" or result = "/bin/cat" +} + +/** + * Predicates for creating an equivalent call to `fs.readFile` from a command execution of `cat`. + */ +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, string fileArg | + (if cat.isSync() then sync = "Sync" else sync = "") and + ( + exists(cat.getOptionsArg()) and + ( + extraArg = ", " + createOptionsArg(cat.getOptionsArg()) + or + not exists(createOptionsArg(cat.getOptionsArg())) and + extraArg = ", ..." + ) + or + extraArg = "" and not exists(cat.getOptionsArg()) + ) and + ( + callback = createCallbackString(cat.getCallback()) + or + 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(containsNonTrivialShellChar(fileArg.regexpReplaceAll("\\$|\\`| ", ""))) // string concat might contain " ", template strings might contain "$" or `, and that is OK. + | + result = + "fs.readFile" + sync + "(" + fileArg + extraArg + callback + ")" + ) + } + + /** + * 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)) + } + + /** + * Create a string representing the callback `func`. + */ + string createCallbackString(DataFlow::FunctionNode func) { + exists(string params | params = createCallbackParams(func) | + if func.getFunction() instanceof ArrowFunctionExpr + then + if func.getFunction().getBody() instanceof Expr + then result = ", (" + params + ") => ..." + else result = ", (" + params + ") => {...}" + else result = ", function(" + params + ") {...}" + ) + } + + /** + * Create a string concatenation of the parameter names in a function `func`. + */ + private string createCallbackParams(DataFlow::FunctionNode func) { + result = + concat(int i | + i = [0 .. func.getNumParameter()] + | + func.getParameter(i).getName(), ", " order by i + ) + } + + /** + * 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(".*\\.\\..*") + } + + /** + * 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 + ) + "`" + ) + } + + /** + * 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 + result = e.(VarAccess).getVariable().getName() + } + + /** + * 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() + "}" + or + result = e.(TemplateElement).getValue() + } + + /** + * 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 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() + "\"" + ) + 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 + exists(string rawConcat | rawConcat = quote + printed.suffix(cat.length()).trim() | + result = createSimplifiedStringConcat(rawConcat) + ) + ) + ) + } + + /** + * Create a simplified and equivalent string concatenation for a given string concatenation `str` + */ + bindingset[str] + private string createSimplifiedStringConcat(string str) { + // Remove an initial ""+ (e.g. in `""+file`) + if str.prefix(5) = "\"\" + " + then result = str.suffix(5) + else + // 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 + } + + /** + * Create the file that is read for a call with an explicit command list (e.g. `child_process.execFile/execFileSync`). + */ + string createFileThatIsReadFromCommandList(CommandCall call) { + exists(DataFlow::ArrayCreationNode array, DataFlow::Node element | + array = call.getArgumentList().(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) + ) + } +} 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..4f2dae3d7b1a --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected @@ -0,0 +1,112 @@ +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, 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) | +| 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) {...}) | +| 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) => {...}) | +| 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") | +| 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: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}) | +| 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}` ]) | +| 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) | +| 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} | +| 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/UselessUseOfCat.ql b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql new file mode 100644 index 000000000000..a70f53016bda --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.ql @@ -0,0 +1,28 @@ +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 + +query string readFile(UselessCat cat) { result = PrettyPrintCatCall::createReadFileCall(cat) } + +query SystemCommandExecution syncCommand() { + result.isSync() +} + +query DataFlow::Node options(SystemCommandExecution sys) { + result = sys.getOptionsArg() +} \ No newline at end of file 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..ace4784b3806 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-078/uselesscat.js @@ -0,0 +1,166 @@ +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(); + +exec("cat foo/bar", function (err, out) {}); // NOT OK + +exec("cat /proc/" + id + "/status", function (err, out) { // NOT OK + console.log(out); +}); + +execSync('cat /proc/cpuinfo').toString(); // NOT OK. + +execSync(`cat ${newpath}`) // NOT OK + +execSync('cat package.json | wc -l'); // OK - pipes! + +execSync('cat /proc/cpuinfo /foo/bar').toString(); // OK multiple files. + +execSync(`cat ${newpath} /foo/bar`).toString(); // OK multiple files. + +exec(`cat ${newpath} | grep foo`, function (err, out) { }) // OK - pipes + +execSync(`cat ${newpath}`, {uid: 1000}) // OK - non trivial options + +exec('cat *.js | wc -l', { cwd: './' }, function (err, out) { }); // OK - wildcard and pipes + +execSync(`cat foo/bar/${newpath}`); // NOT OK ("encoding" is used EXACTLY the same way in fs.readFileSync) + +execSync(`cat foo/bar/${newpath}`, {encoding: 'utf8'}); // 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)) + +execSync('cat /proc/cpuinfo > foo/bar/baz').toString(); // OK. + +execSync(`cat ${newpath} > ${destpath}`).toString(); // OK. + +execSync(`cat ${files.join(' ')} > ${outFile}`); // OK + +execSync(`cat ${files.join(' ')}`); // OK [but flagged] - not just a simple file read + +exec("cat /proc/cpuinfo | grep name"); // OK - pipes + +execSync(`cat ${newpath} | ${othertool}`); // OK - pipes + +function cat(file) { + return execSync('cat ' + file).toString(); // NOT OK +} + +execSync("sh -c 'cat " + newpath + "'"); // NOT OK. [but not flagged] + +var execFile = child_process.execFile; +var execFileSync = child_process.execFileSync; + +execFile('/bin/cat', [ 'pom.xml' ], function(error, stdout, stderr ) { // NOT OK + // Not using stderr + console.log(stdout); +}); + +execFile('/bin/cat', [ 'pom.xml' ], function(error, stdout, stderr ) { // OK. - stderr is used. + console.log(stderr); +}); + + +execFile('/bin/cat', [ 'pom.xml' ], {encoding: 'utf8'}, function(error, stdout, stderr ) { // NOT OK + // Not using stderr + console.log(stdout); +}); + +execFileSync('/bin/cat', [ 'pom.xml' ], {encoding: 'utf8'}); // NOT OK + +execFileSync('/bin/cat', [ 'pom.xml' ]); // NOT OK + +var opts = {encoding: 'utf8'}; +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 + +execFileSync('/bin/cat', [ "foo/" + newPath + "bar" ], {encoding: 'utf8'}); // NOT OK + +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 + +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. +})(); + +const stdout2 = execSync('cat /etc/dnsmasq.conf', { // NOT OK. + encoding: 'utf8' +}); + +exec('/bin/cat', function (e, s) {}); // OK + +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