mirror of
https://github.com/github/codeql.git
synced 2026-04-15 20:14:02 +02:00
212 lines
7.2 KiB
Plaintext
212 lines
7.2 KiB
Plaintext
/**
|
|
* Provides default sources, sinks and sanitizers for reasoning about
|
|
* command-injection vulnerabilities, as well as extension points for
|
|
* adding your own.
|
|
*/
|
|
|
|
import javascript
|
|
|
|
module IndirectCommandInjection {
|
|
/**
|
|
* A data flow source for command-injection vulnerabilities.
|
|
*/
|
|
abstract class Source extends DataFlow::Node {
|
|
/** Gets a description of this source. */
|
|
string describe() { result = "command-line argument" }
|
|
}
|
|
|
|
/**
|
|
* A data flow sink for command-injection vulnerabilities.
|
|
*/
|
|
abstract class Sink extends DataFlow::Node { }
|
|
|
|
/**
|
|
* A sanitizer for command-injection vulnerabilities.
|
|
*/
|
|
abstract class Sanitizer extends DataFlow::Node { }
|
|
|
|
/**
|
|
* A source of user input from the command-line, considered as a flow source for command injection.
|
|
*/
|
|
private class CommandLineArgumentsArrayAsSource extends Source instanceof CommandLineArgumentsArray
|
|
{ }
|
|
|
|
/**
|
|
* An array of command-line arguments.
|
|
*/
|
|
class CommandLineArgumentsArray extends DataFlow::SourceNode {
|
|
CommandLineArgumentsArray() {
|
|
this = DataFlow::globalVarRef("process").getAPropertyRead("argv")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A read of `process.env`, considered as a flow source for command injection.
|
|
*/
|
|
private class ProcessEnvAsSource extends Source {
|
|
ProcessEnvAsSource() { this = NodeJSLib::process().getAPropertyRead("env") }
|
|
|
|
override string describe() { result = "environment variable" }
|
|
}
|
|
|
|
/** Gets a data flow node referring to `process.env`. */
|
|
private DataFlow::SourceNode envObject(DataFlow::TypeTracker t) {
|
|
t.start() and
|
|
result = NodeJSLib::process().getAPropertyRead("env")
|
|
or
|
|
exists(DataFlow::TypeTracker t2 | result = envObject(t2).track(t2, t))
|
|
}
|
|
|
|
/** Gets a data flow node referring to `process.env`. */
|
|
private DataFlow::SourceNode envObject() { result = envObject(DataFlow::TypeTracker::end()) }
|
|
|
|
/**
|
|
* Gets the name of an environment variable that is assumed to be safe.
|
|
*/
|
|
private string getASafeEnvironmentVariable() {
|
|
result =
|
|
[
|
|
"GITHUB_ACTION", "GITHUB_ACTION_PATH", "GITHUB_ACTION_REPOSITORY", "GITHUB_ACTIONS",
|
|
"GITHUB_ACTOR", "GITHUB_API_URL", "GITHUB_BASE_REF", "GITHUB_ENV", "GITHUB_EVENT_NAME",
|
|
"GITHUB_EVENT_PATH", "GITHUB_GRAPHQL_URL", "GITHUB_JOB", "GITHUB_PATH", "GITHUB_REF",
|
|
"GITHUB_REPOSITORY", "GITHUB_REPOSITORY_OWNER", "GITHUB_RUN_ID", "GITHUB_RUN_NUMBER",
|
|
"GITHUB_SERVER_URL", "GITHUB_SHA", "GITHUB_WORKFLOW", "GITHUB_WORKSPACE"
|
|
]
|
|
}
|
|
|
|
/** Sanitizer that blocks flow through safe environment variables. */
|
|
private class SafeEnvVariableSanitizer extends Sanitizer {
|
|
SafeEnvVariableSanitizer() {
|
|
this = envObject().getAPropertyRead(getASafeEnvironmentVariable())
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An object containing parsed command-line arguments, considered as a flow source for command injection.
|
|
*/
|
|
class ParsedCommandLineArgumentsAsSource extends Source {
|
|
ParsedCommandLineArgumentsAsSource() {
|
|
// `require('get-them-args')(...)` => `{ unknown: [], a: ... b: ... }`
|
|
this = DataFlow::moduleImport("get-them-args").getACall()
|
|
or
|
|
// `require('optimist').argv` => `{ _: [], a: ... b: ... }`
|
|
this = DataFlow::moduleMember("optimist", "argv")
|
|
or
|
|
// `require("arg")({...spec})` => `{_: [], a: ..., b: ...}`
|
|
this = DataFlow::moduleImport("arg").getACall()
|
|
or
|
|
// `(new (require(argparse)).ArgumentParser({...spec})).parse_args()` => `{a: ..., b: ...}`
|
|
this =
|
|
API::moduleImport("argparse")
|
|
.getMember("ArgumentParser")
|
|
.getInstance()
|
|
.getMember("parse_args")
|
|
.getACall()
|
|
or
|
|
// `require('command-line-args')({...spec})` => `{a: ..., b: ...}`
|
|
this = DataFlow::moduleImport("command-line-args").getACall()
|
|
or
|
|
// `require('meow')(help, {...spec})` => `{a: ..., b: ....}`
|
|
this = DataFlow::moduleImport("meow").getACall()
|
|
or
|
|
// `require("dashdash").createParser(...spec)` => `{a: ..., b: ...}`
|
|
this =
|
|
[
|
|
API::moduleImport("dashdash"),
|
|
API::moduleImport("dashdash").getMember("createParser").getReturn()
|
|
].getMember("parse").getACall()
|
|
or
|
|
// `require('commander').myCmdArgumentName`
|
|
this = commander().getAMember().asSource()
|
|
or
|
|
// `require('commander').opt()` => `{a: ..., b: ...}`
|
|
this = commander().getMember("opts").getACall()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Holds if there is a command line parsing step from `pred` to `succ`.
|
|
* E.g: `var succ = require("minimist")(pred)`.
|
|
*/
|
|
predicate argsParseStep(DataFlow::Node pred, DataFlow::Node succ) {
|
|
exists(DataFlow::CallNode call |
|
|
call = DataFlow::moduleMember("args", "parse").getACall() or
|
|
call = DataFlow::moduleImport(["yargs-parser", "minimist", "subarg"]).getACall()
|
|
|
|
|
succ = call and
|
|
pred = call.getArgument(0)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets a Command instance from the `commander` library.
|
|
*/
|
|
private API::Node commander() {
|
|
result = API::moduleImport("commander")
|
|
or
|
|
// `require("commander").program === require("commander")`
|
|
result = commander().getMember("program")
|
|
or
|
|
result = commander().getMember("Command").getInstance()
|
|
or
|
|
// lots of chainable methods
|
|
result = commander().getAMember().getReturn()
|
|
}
|
|
|
|
/**
|
|
* Gets an instance of `yargs`.
|
|
* Either directly imported as a module, or through some chained method call.
|
|
*/
|
|
private DataFlow::SourceNode yargs() {
|
|
result = DataFlow::moduleImport("yargs")
|
|
or
|
|
// script used to generate list of chained methods: https://gist.github.com/erik-krogh/f8afe952c0577f4b563a993e613269ba
|
|
exists(string method |
|
|
not method =
|
|
// the methods that does not return a chained `yargs` object.
|
|
[
|
|
"getContext", "getDemandedOptions", "getDemandedCommands", "getDeprecatedOptions",
|
|
"_getParseContext", "getOptions", "getGroups", "getStrict", "getStrictCommands",
|
|
"getExitProcess", "locale", "getUsageInstance", "getCommandInstance"
|
|
]
|
|
|
|
|
result = yargs().getAMethodCall(method)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* An array of command line arguments (`argv`) parsed by the `yargs` library.
|
|
*/
|
|
class YargsArgv extends Source {
|
|
YargsArgv() {
|
|
this = yargs().getAPropertyRead("argv")
|
|
or
|
|
this = yargs().getAMethodCall("parse") and
|
|
this.(DataFlow::MethodCallNode).getNumArgument() = 0
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A command-line argument that effectively is system-controlled, and therefore not likely to be exploitable when used in the execution of another command.
|
|
*/
|
|
private class SystemControlledCommandLineArgumentSanitizer extends Sanitizer {
|
|
SystemControlledCommandLineArgumentSanitizer() {
|
|
// `process.argv[0]` and `process.argv[1]` are paths to `node` and `main`.
|
|
exists(string index | index = "0" or index = "1" |
|
|
this = any(CommandLineArgumentsArray a).getAPropertyRead(index)
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A command argument to a function that initiates an operating system command as a shell invocation.
|
|
*/
|
|
private class SystemCommandExecutionSink extends Sink, DataFlow::ValueNode {
|
|
SystemCommandExecutionSink() {
|
|
exists(SystemCommandExecution sys |
|
|
sys.isShellInterpreted(this) and this = sys.getACommandArgument()
|
|
)
|
|
}
|
|
}
|
|
}
|