Files
codeql/javascript/ql/lib/semmle/javascript/frameworks/Execa.qll

297 lines
9.2 KiB
Plaintext

/**
* Models the `execa` library in terms of `FileSystemAccess` and `SystemCommandExecution`.
*/
import javascript
/**
* Provide model for [Execa](https://github.com/sindresorhus/execa) package
*/
module Execa {
/**
* The Execa input file read and output file write
*/
class ExecaFileSystemAccess extends FileSystemReadAccess, DataFlow::Node {
API::Node execaArg;
boolean isPipedToFile;
ExecaFileSystemAccess() {
(
execaArg = API::moduleImport("execa").getMember("$").getParameter(0) and
isPipedToFile = false
or
execaArg =
API::moduleImport("execa")
.getMember(["execa", "execaCommand", "execaCommandSync", "execaSync"])
.getParameter([0, 1, 2]) and
isPipedToFile = false
or
execaArg =
API::moduleImport("execa")
.getMember(["execa", "execaCommand", "execaCommandSync", "execaSync"])
.getReturn()
.getMember(["pipeStdout", "pipeAll", "pipeStderr"])
.getParameter(0) and
isPipedToFile = true
) and
this = execaArg.asSink()
}
override DataFlow::Node getADataNode() { none() }
override DataFlow::Node getAPathArgument() {
result = execaArg.getMember("inputFile").asSink() and isPipedToFile = false
or
result = execaArg.asSink() and isPipedToFile = true
}
}
/**
* A call to `execa.execa` or `execa.execaSync`
*/
class ExecaCall extends API::CallNode {
boolean isSync;
ExecaCall() {
this = API::moduleImport("execa").getMember("execa").getACall() and
isSync = false
or
this = API::moduleImport("execa").getMember("execaSync").getACall() and
isSync = true
or
this = API::moduleImport("execa").getACall() and
isSync = false
}
}
/**
* The system command execution nodes for `execa.execa` or `execa.execaSync` functions
*/
class ExecaExec extends SystemCommandExecution, ExecaCall {
ExecaExec() { isSync = [false, true] }
override DataFlow::Node getACommandArgument() { result = this.getArgument(0) }
override predicate isShellInterpreted(DataFlow::Node arg) {
// if shell: true then first and second args are sinks
// options can be third argument
arg = [this.getArgument(0), this.getParameter(1).getArrayElement().asSink()] and
isExecaShellEnable(this.getParameter(2))
or
// options can be second argument
arg = this.getArgument(0) and
isExecaShellEnable(this.getParameter(1))
}
override DataFlow::Node getArgumentList() {
// execa(cmd, [arg]);
exists(DataFlow::Node arg | arg = this.getArgument(1) |
// if it is a object then it is a option argument not command argument
result = arg and not arg.asExpr() instanceof ObjectExpr
)
}
override predicate isSync() { isSync = true }
override DataFlow::Node getOptionsArg() {
result = this.getLastArgument() and result.asExpr() instanceof ObjectExpr
}
}
/**
* A call to `execa.$` or `execa.$.sync` or `execa.$({})` or `execa.$.sync({})` tag functions
*/
private class ExecaScriptCall extends API::CallNode {
boolean isSync;
ExecaScriptCall() {
exists(API::Node script |
script =
[
API::moduleImport("execa").getMember("$"),
API::moduleImport("execa").getMember("$").getReturn()
]
|
this = script.getACall() and
isSync = false
or
this = script.getMember("sync").getACall() and
isSync = true
)
}
}
/**
* The system command execution nodes for `execa.$` or `execa.$.sync` tag functions
*/
class ExecaScript extends SystemCommandExecution, ExecaScriptCall {
ExecaScript() { isSync = [false, true] }
override DataFlow::Node getACommandArgument() {
result = this.getParameter(1).asSink() and
not isTaggedTemplateFirstChildAnElement(this.getParameter(1).asSink().asExpr().getParent())
}
override predicate isShellInterpreted(DataFlow::Node arg) {
isExecaShellEnable(this.getParameter(0)) and
arg = this.getAParameter().asSink()
}
override DataFlow::Node getArgumentList() {
result = this.getParameter(any(int i | i >= 1)).asSink() and
isTaggedTemplateFirstChildAnElement(this.getParameter(1).asSink().asExpr().getParent())
or
result = this.getParameter(any(int i | i >= 2)).asSink() and
not isTaggedTemplateFirstChildAnElement(this.getParameter(1).asSink().asExpr().getParent())
}
override DataFlow::Node getOptionsArg() { result = this.getParameter(0).asSink() }
override predicate isSync() { isSync = true }
}
/**
* A call to `execa.execaCommandSync` or `execa.execaCommand`
*/
private class ExecaCommandCall extends API::CallNode {
boolean isSync;
ExecaCommandCall() {
this = API::moduleImport("execa").getMember("execaCommandSync").getACall() and
isSync = true
or
this = API::moduleImport("execa").getMember("execaCommand").getACall() and
isSync = false
}
}
/**
* The system command execution nodes for `execa.execaCommand` or `execa.execaCommandSync` functions
*/
class ExecaCommandExec extends SystemCommandExecution, ExecaCommandCall {
ExecaCommandExec() { isSync = [false, true] }
override DataFlow::Node getACommandArgument() {
result = this.(DataFlow::CallNode).getArgument(0)
}
override DataFlow::Node getArgumentList() {
// execaCommand(`${cmd} ${arg}`);
result.asExpr() = this.getParameter(0).asSink().asExpr().getAChildExpr() and
not result.asExpr() = this.getArgument(0).asExpr().getChildExpr(0)
}
override predicate isShellInterpreted(DataFlow::Node arg) {
// execaCommandSync(`${cmd} ${arg}`, {shell: true})
arg.asExpr() = this.getArgument(0).asExpr().getAChildExpr+() and
isExecaShellEnable(this.getParameter(1))
or
// there is only one argument that is constructed in previous nodes,
// it makes sanitizing really hard to select whether it is vulnerable to argument injection or not
arg = this.getParameter(0).asSink() and
not exists(this.getArgument(0).asExpr().getChildExpr(1))
}
override predicate isSync() { isSync = true }
override DataFlow::Node getOptionsArg() {
result = this.getLastArgument() and result.asExpr() instanceof ObjectExpr
}
}
/** Gets a TemplateLiteral and check if first child is a template element */
private predicate isTaggedTemplateFirstChildAnElement(TemplateLiteral templateLit) {
exists(templateLit.getChildExpr(0).(TemplateElement))
}
/**
* Holds whether Execa has shell enabled options or not, get Parameter responsible for options
*/
pragma[inline]
private predicate isExecaShellEnable(API::Node n) {
n.getMember("shell").asSink().asExpr().(BooleanLiteral).getValue() = "true"
}
/**
* A call to `execa.node`
*/
class ExecaNodeCall extends SystemCommandExecution, API::CallNode {
ExecaNodeCall() { this = API::moduleImport("execa").getMember("node").getACall() }
override DataFlow::Node getACommandArgument() { result = this.getArgument(0) }
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
override DataFlow::Node getArgumentList() {
result = this.getArgument(1) and
not result.asExpr() instanceof ObjectExpr
}
override predicate isSync() { none() }
override DataFlow::Node getOptionsArg() {
result = this.getLastArgument() and
result.asExpr() instanceof ObjectExpr
}
}
/**
* A call to `execa.stdout`, `execa.stderr`, or `execa.sync`
*/
class ExecaStreamCall extends SystemCommandExecution, API::CallNode {
string methodName;
ExecaStreamCall() {
methodName in ["stdout", "stderr", "sync"] and
this = API::moduleImport("execa").getMember(methodName).getACall()
}
override DataFlow::Node getACommandArgument() { result = this.getArgument(0) }
override predicate isShellInterpreted(DataFlow::Node arg) {
arg = this.getArgument(0) and
isExecaShellEnable(this.getParameter([1, 2]))
}
override DataFlow::Node getArgumentList() {
result = this.getArgument(1) and
not result.asExpr() instanceof ObjectExpr
}
override predicate isSync() { methodName = "sync" }
override DataFlow::Node getOptionsArg() {
result = this.getLastArgument() and
result.asExpr() instanceof ObjectExpr
}
}
/**
* A call to `execa.shell` or `execa.shellSync`
*/
class ExecaShellCall extends SystemCommandExecution, API::CallNode {
boolean sync;
ExecaShellCall() {
this = API::moduleImport("execa").getMember("shell").getACall() and
sync = false
or
this = API::moduleImport("execa").getMember("shellSync").getACall() and
sync = true
}
override DataFlow::Node getACommandArgument() { result = this.getArgument(0) }
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getACommandArgument() }
override DataFlow::Node getArgumentList() { none() }
override predicate isSync() { sync = true }
override DataFlow::Node getOptionsArg() {
result = this.getArgument(1) and
result.asExpr() instanceof ObjectExpr
}
}
}