change useless cat query to only flag instances that can be re-written to

This commit is contained in:
Erik Krogh Kristensen
2020-02-19 14:31:03 +01:00
parent 344060e139
commit bdab9ee12b
6 changed files with 377 additions and 89 deletions

View File

@@ -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)

View File

@@ -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 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()
}
/**
* 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
)
)
)
}
/**
* Gets the dataflow node determining the command executed.
* A call to child_process.exec that might be a useless call to cat.
*/
DataFlow::Node getCommand() { result = command }
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() }
}
}