Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Command injection modeling now recognizes argument arrays passed to `spawn` and `execFile` when the `shell` option is enabled, including string-valued shell paths and calls to `execFile` without a callback.
Original file line number Diff line number Diff line change
Expand Up @@ -813,8 +813,8 @@ module NodeJSLib {
methodName.matches("%Sync")
then result = this.getLastArgument()
else
// the rest (exec/execFile) has the options argument as their second last.
result = this.getParameter(this.getNumArgument() - 2).asSink()
// exec/execFile may have a callback after the options argument.
result = this.getParameter([this.getNumArgument() - 2, this.getNumArgument() - 1]).asSink()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import javascript
private import UnsafeShellCommandConstructionCustomizations

module CommandInjection {
/**
Expand Down Expand Up @@ -55,6 +56,19 @@ module CommandInjection {
SystemCommandExecutionSink() { this = any(SystemCommandExecution sys).getACommandArgument() }
}

/** An argument in a `child_process` argument array that is interpreted by a shell. */
private class ChildProcessArgumentSink extends Sink {
ChildProcessArgumentSink() {
exists(UnsafeShellCommandConstruction::ShellTrueCommandExecutionSink sink |
this = sink and
sink.getCommandExecution() =
API::moduleImport(["mz/child_process", "child_process"])
.getMember(["execFile", "execFileSync", "spawn", "spawnSync"])
.getMaybePromisifiedCall()
)
}
}

private class SinkFromModel extends Sink {
SinkFromModel() { ModelOutput::sinkNode(this, "command-injection") }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,20 +175,20 @@ module UnsafeShellCommandConstruction {
}

/**
* Holds if the arguments array given to `sys` is joined as a string because `shell` is set to true.
* Holds if the arguments array given to `sys` is joined as a string because the `shell` option is enabled.
*/
predicate executesArrayAsShell(SystemCommandExecution sys) {
sys.getOptionsArg()
.getALocalSource()
.getAPropertyWrite("shell")
.getRhs()
.asExpr()
.(BooleanLiteral)
.getValue() = "true"
or
exists(API::Node node |
node.asSink() = sys.getOptionsArg() and
node.getMember("shell").asSink().mayHaveBooleanValue(true)
exists(DataFlow::Node shell |
shell = sys.getOptionsArg().getALocalSource().getAPropertyWrite("shell").getRhs()
or
exists(API::Node node |
node.asSink() = sys.getOptionsArg() and
shell = node.getMember("shell").asSink()
)
|
shell.mayHaveBooleanValue(true)
or
exists(string path | shell.mayHaveStringValue(path) and path != "")
)
}

Expand All @@ -208,13 +208,13 @@ module UnsafeShellCommandConstruction {
}

/**
* An argument to a command invocation where the `shell` option is set to true.
* An argument to a command invocation where the `shell` option is enabled.
*/
class ShellTrueCommandExecutionSink extends Sink {
SystemCommandExecution sys;

ShellTrueCommandExecutionSink() {
// `shell` is set to true. That means string-concatenation happens behind the scenes.
// `shell` is enabled. That means string-concatenation happens behind the scenes.
// We just assume that a `shell` option in any library means the same thing as it does in NodeJS.
exists(DataFlow::SourceNode arr |
arr = endsInShellExecutedArray(DataFlow::TypeBackTracker::end(), sys)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The `js/command-line-injection` query now reports remote input that reaches argument arrays when the `shell` option is enabled. The `js/shell-command-constructed-from-input` query also recognizes string-valued shell paths. Both queries now handle `execFile` calls without a callback.