Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions javascript/ql/lib/javascript.qll
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ import semmle.javascript.frameworks.Request
import semmle.javascript.frameworks.RxJS
import semmle.javascript.frameworks.ServerLess
import semmle.javascript.frameworks.ShellJS
import semmle.javascript.frameworks.Execa
import semmle.javascript.frameworks.Snapdragon
import semmle.javascript.frameworks.SystemCommandExecutors
import semmle.javascript.frameworks.SQL
Expand Down
250 changes: 250 additions & 0 deletions javascript/ql/lib/semmle/javascript/frameworks/Execa.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
/**
* Models the `execa` library in terms of `FileSystemAccess` and `SystemCommandExecution`.
*/

import javascript
import semmle.javascript.security.dataflow.RequestForgeryCustomizations
import semmle.javascript.security.dataflow.UrlConcatenation

/**
* The dynamic import expression input can be a `data:` URL which loads any module from that data
*/
class DynamicImport extends SystemCommandExecution, DataFlow::ExprNode {
DynamicImport() { this = any(DynamicImportExpr e).getAChildExpr().flow() }

override DataFlow::Node getACommandArgument() { result = this }

override predicate isShellInterpreted(DataFlow::Node arg) { none() }

override predicate isSync() { none() }

override DataFlow::Node getOptionsArg() { none() }
}

/**
* Provide model for [Execa](https://github.com/sindresorhus/execa) package
*/
module Execa {
/**
* The Execa input file option
*/
class ExecaRead extends FileSystemReadAccess, DataFlow::Node {
API::Node execaNode;

ExecaRead() {
(
execaNode = API::moduleImport("execa").getMember("$").getParameter(0)
or
execaNode =
API::moduleImport("execa")
.getMember(["execa", "execaCommand", "execaCommandSync", "execaSync"])
.getParameter([0, 1, 2])
) and
this = execaNode.asSink()
}

// data is the output of a command so IDK how it can be implemented
override DataFlow::Node getADataNode() { none() }

override DataFlow::Node getAPathArgument() {
result = execaNode.getMember("inputFile").asSink()
}
}

/**
* A call to `execa.execa` or `execa.execaSync`
*/
class ExecaCall extends API::CallNode {
string name;

ExecaCall() {
this = API::moduleImport("execa").getMember("execa").getACall() and
name = "execa"
or
this = API::moduleImport("execa").getMember("execaSync").getACall() and
name = "execaSync"
}

/** Gets the name of the exported function, such as `rm` in `shelljs.rm()`. */
string getName() { result = name }
}

/**
* The system command execution nodes for `execa.execa` or `execa.execaSync` functions
*/
class ExecaExec extends SystemCommandExecution, ExecaCall {
ExecaExec() { name = ["execa", "execaSync"] }

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).getUnknownMember().asSink()] and
isExecaShellEnable(this.getParameter(2))
or
// options can be second argument
arg = this.getArgument(0) and
isExecaShellEnable(this.getParameter(1))
}

override predicate isSync() { name = "execaSync" }

override DataFlow::Node getOptionsArg() {
result = this.getLastArgument() and result.asExpr() instanceof ObjectExpr
}
}

/**
* A call to `execa.$` or `execa.$.sync` tag functions
*/
private class ExecaScriptExpr extends DataFlow::ExprNode {
string name;

ExecaScriptExpr() {
this.asExpr() =
[
API::moduleImport("execa").getMember("$"),
API::moduleImport("execa").getMember("$").getReturn()
].getAValueReachableFromSource().asExpr() and
name = "ASync"
or
this.asExpr() =
[
API::moduleImport("execa").getMember("$").getMember("sync"),
API::moduleImport("execa").getMember("$").getMember("sync").getReturn()
].getAValueReachableFromSource().asExpr() and
name = "Sync"
}

/** Gets the name of the exported function, such as `rm` in `shelljs.rm()`. */
string getName() { result = name }
}

/**
* The system command execution nodes for `execa.$` or `execa.$.sync` tag functions
*/
class ExecaScriptEec extends SystemCommandExecution, ExecaScriptExpr {
ExecaScriptEec() { name = ["Sync", "ASync"] }

override DataFlow::Node getACommandArgument() {
result.asExpr() = templateLiteralChildAsSink(this.asExpr()).getChildExpr(0)
}

override predicate isShellInterpreted(DataFlow::Node arg) {
// $({shell: true})`${sink} ${sink} .. ${sink}`
// ISSUE: $`cmd args` I can't reach the tag function argument easily
exists(TemplateLiteral tmpL | templateLiteralChildAsSink(this.asExpr()) = tmpL |
arg.asExpr() = tmpL.getAChildExpr+() and
isExecaShellEnableWithExpr(this.asExpr().(CallExpr).getArgument(0))
)
}

override DataFlow::Node getArgumentList() {
// $`${Can Not Be sink} ${sink} .. ${sink}`
exists(TemplateLiteral tmpL | templateLiteralChildAsSink(this.asExpr()) = tmpL |
result.asExpr() = tmpL.getAChildExpr+() and
not result.asExpr() = tmpL.getChildExpr(0)
)
}

override predicate isSync() { name = "Sync" }

override DataFlow::Node getOptionsArg() {
result = this.asExpr().getAChildExpr*().flow() and result.asExpr() instanceof ObjectExpr
}
}

/**
* A call to `execa.execaCommandSync` or `execa.execaCommand`
*/
private class ExecaCommandCall extends API::CallNode {
string name;

ExecaCommandCall() {
this = API::moduleImport("execa").getMember("execaCommandSync").getACall() and
name = "execaCommandSync"
or
this = API::moduleImport("execa").getMember("execaCommand").getACall() and
name = "execaCommand"
}

/** Gets the name of the exported function, such as `rm` in `shelljs.rm()`. */
string getName() { result = name }
}

/**
* The system command execution nodes for `execa.execaCommand` or `execa.execaCommandSync` functions
*/
class ExecaCommandExec2 extends SystemCommandExecution, DataFlow::CallNode {
ExecaCommandExec2() { this = API::moduleImport("execa").getMember("execaCommand").getACall() }

override DataFlow::Node getACommandArgument() { result = this.getArgument(0) }

override DataFlow::Node getArgumentList() { result = this.getArgument(0) }

override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getArgument(0) }

override predicate isSync() { none() }

override DataFlow::Node getOptionsArg() { result = this }
}

/**
* The system command execution nodes for `execa.execaCommand` or `execa.execaCommandSync` functions
*/
class ExecaCommandExec extends SystemCommandExecution, ExecaCommandCall {
ExecaCommandExec() { name = ["execaCommand", "execaCommandSync"] }

override DataFlow::Node getACommandArgument() {
result = this.(DataFlow::CallNode).getArgument(0)
}

override DataFlow::Node getArgumentList() {
// execaCommand("echo " + sink);
// execaCommand(`echo ${sink}`);
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(sink1 + sink2, {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() { name = "execaCommandSync" }

override DataFlow::Node getOptionsArg() {
result = this.getLastArgument() and result.asExpr() instanceof ObjectExpr
}
}

// Holds if left parameter is the the left child of a template literal and returns the template literal

Check warning

Code scanning / CodeQL

Comment has repeated word

The comment repeats the.
private TemplateLiteral templateLiteralChildAsSink(Expr left) {
exists(TaggedTemplateExpr parent |
parent.getTemplate() = result and
left = parent.getChildExpr(0)
)
}

// Holds whether Execa has shell enabled options or not, get Parameter responsible for options
private predicate isExecaShellEnable(API::Node n) {
n.getMember("shell").asSink().asExpr().(BooleanLiteral).getValue() = "true" and
exists(n.getMember("shell"))

Check warning

Code scanning / CodeQL

Superfluous 'exists' conjunct.

This conjunct is superfluous as the existence is implied by [this conjunct](1).
}

// Holds whether Execa has shell enabled options or not, get Parameter responsible for options
private predicate isExecaShellEnableWithExpr(Expr n) {
exists(ObjectExpr o, Property p | o = n.getAChildExpr*() |
o.getAChild() = p and
p.getAChild().(Label).getName() = "shell" and
p.getAChild().(Literal).getValue() = "true"
)
}
}
23 changes: 21 additions & 2 deletions javascript/ql/lib/semmle/javascript/frameworks/ShellJS.qll
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
/**
* Models the `shelljs` library in terms of `FileSystemAccess` and `SystemCommandExecution`.
*
* https://www.npmjs.com/package/shelljs
*/

import javascript

module ShellJS {
API::Node shellJSMember() {
result = API::moduleImport("shelljs")
or
result =
shellJSMember()
.getMember([
"exec", "cd", "cp", "touch", "chmod", "pushd", "find", "ls", "ln", "mkdir", "mv",
"rm", "cat", "head", "sort", "tail", "uniq", "grep", "sed", "to", "toEnd", "echo"
])
.getReturn()
}

API::Node test() { result = API::moduleImport("shelljs").getASuccessor*() }

/**
* Gets an import of the `shelljs` or `async-shelljs` module.
*/
DataFlow::SourceNode shelljs() {
result = DataFlow::moduleImport("shelljs") or
result = shellJSMember().asSource() or
result = DataFlow::moduleImport("async-shelljs")
}

Expand Down Expand Up @@ -39,7 +55,10 @@ module ShellJS {

/** The `shelljs.exec` library modeled as a `shelljs` member. */
private class ShellJsExec extends Range {
ShellJsExec() { this = DataFlow::moduleImport("shelljs.exec") }
ShellJsExec() {
this = DataFlow::moduleImport("shelljs.exec") or
this = shellJSMember().getMember("exec").asSource()
}

override string getName() { result = "exec" }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
test_FileSystemAccess
| tst.js:18:9:18:23 | { shell: true } |
| tst.js:20:9:20:24 | { shell: false } |
| tst.js:24:13:24:22 | 'aCommand' |
| tst.js:24:25:24:36 | ['example1'] |
| tst.js:26:13:26:18 | 'echo' |
| tst.js:26:21:26:32 | ['example1'] |
| tst.js:28:13:28:47 | 'echo e ... ple 11' |
| tst.js:28:50:28:64 | { shell: true } |
| tst.js:29:13:29:29 | 'echo example 10' |
| tst.js:29:32:29:52 | ['; ech ... le 11'] |
| tst.js:29:55:29:69 | { shell: true } |
| tst.js:32:11:32:16 | 'echo' |
| tst.js:32:19:32:35 | ['example5 sync'] |
| tst.js:34:20:34:42 | "echo " ... gument" |
| tst.js:35:20:35:52 | `echo $ ... ndSync` |
| tst.js:37:18:37:20 | arg |
| tst.js:39:18:39:39 | "echo 1 ... echo 2" |
| tst.js:39:42:39:56 | { shell: true } |
| tst.js:45:9:45:27 | { inputFile: file } |
| tst.js:46:13:46:17 | 'cat' |
| tst.js:46:20:46:38 | { inputFile: file } |
| tst.js:47:13:47:18 | 'echo' |
| tst.js:47:21:47:32 | ['example2'] |
| tst.js:48:13:48:18 | 'echo' |
| tst.js:48:21:48:32 | ['example3'] |
| tst.js:49:13:49:18 | 'echo' |
| tst.js:49:21:49:32 | ['example4'] |
| tst.js:49:35:49:47 | { all: true } |
test_MissingFileSystemAccess
| tst.js:43:35:43:38 | file |
| tst.js:47:46:47:49 | file |
| tst.js:48:46:48:49 | file |
| tst.js:49:58:49:61 | file |
test_SystemCommandExecution
| tst.js:1:71:1:71 | $ |
| tst.js:4:7:4:7 | $ |
| tst.js:5:7:5:7 | $ |
| tst.js:6:1:6:1 | $ |
| tst.js:6:1:6:6 | $.sync |
| tst.js:10:7:10:7 | $ |
| tst.js:12:7:12:7 | $ |
| tst.js:13:1:13:1 | $ |
| tst.js:13:1:13:6 | $.sync |
| tst.js:15:1:15:1 | $ |
| tst.js:15:1:15:6 | $.sync |
| tst.js:16:7:16:7 | $ |
| tst.js:18:7:18:7 | $ |
| tst.js:18:7:18:24 | $({ shell: true }) |
| tst.js:20:7:20:7 | $ |
| tst.js:20:7:20:25 | $({ shell: false }) |
| tst.js:24:7:24:37 | execa(' ... ple1']) |
| tst.js:26:7:26:33 | execa(' ... ple1']) |
| tst.js:28:7:28:65 | execa(' ... true }) |
| tst.js:29:7:29:70 | execa(' ... true }) |
| tst.js:32:1:32:36 | execaSy ... sync']) |
| tst.js:34:7:34:43 | execaCo ... ument") |
| tst.js:35:7:35:53 | execaCo ... dSync`) |
| tst.js:37:1:37:21 | execaCo ... nc(arg) |
| tst.js:39:1:39:57 | execaCo ... true }) |
| tst.js:43:7:43:7 | $ |
| tst.js:45:7:45:7 | $ |
| tst.js:45:7:45:28 | $({ inp ... file }) |
| tst.js:46:7:46:39 | execa(' ... file }) |
| tst.js:47:7:47:33 | execa(' ... ple2']) |
| tst.js:48:7:48:33 | execa(' ... ple3']) |
| tst.js:49:7:49:48 | execa(' ... true }) |
test_FileNameSource
12 changes: 12 additions & 0 deletions javascript/ql/test/library-tests/frameworks/Execa/Execa.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import javascript

query predicate test_FileSystemAccess(FileSystemAccess access) { any() }

query predicate test_MissingFileSystemAccess(VarAccess var) {
var.getName().matches("file%") and
not exists(FileSystemAccess access | access.getAPathArgument().asExpr() = var)
}

query predicate test_SystemCommandExecution(SystemCommandExecution exec) { any() }

query predicate test_FileNameSource(FileNameSource exec) { any() }
Loading