mirror of
https://github.com/github/codeql.git
synced 2026-04-30 03:05:15 +02:00
@@ -12,6 +12,7 @@ import core.Module
|
||||
import core.Array
|
||||
import core.String
|
||||
import core.Regexp
|
||||
import core.IO
|
||||
|
||||
/**
|
||||
* A system command executed via subshell literal syntax.
|
||||
|
||||
@@ -6,267 +6,10 @@ private import ruby
|
||||
private import codeql.ruby.Concepts
|
||||
private import codeql.ruby.ApiGraphs
|
||||
private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.frameworks.Core
|
||||
private import codeql.ruby.dataflow.FlowSummary
|
||||
|
||||
private DataFlow::Node ioInstanceInstantiation() {
|
||||
result = API::getTopLevelMember("IO").getAnInstantiation() or
|
||||
result = API::getTopLevelMember("IO").getAMethodCall(["for_fd", "open", "try_convert"])
|
||||
}
|
||||
|
||||
private DataFlow::Node ioInstance() {
|
||||
result = ioInstanceInstantiation()
|
||||
or
|
||||
exists(DataFlow::Node inst |
|
||||
inst = ioInstance() and
|
||||
inst.(DataFlow::LocalSourceNode).flowsTo(result)
|
||||
)
|
||||
}
|
||||
|
||||
// Match some simple cases where a path argument specifies a shell command to
|
||||
// be executed. For example, the `"|date"` argument in `IO.read("|date")`, which
|
||||
// will execute a shell command and read its output rather than reading from the
|
||||
// filesystem.
|
||||
private predicate pathArgSpawnsSubprocess(Expr arg) {
|
||||
arg.getConstantValue().getStringlikeValue().charAt(0) = "|"
|
||||
}
|
||||
|
||||
private DataFlow::Node fileInstanceInstantiation() {
|
||||
result = API::getTopLevelMember("File").getAnInstantiation()
|
||||
or
|
||||
result = API::getTopLevelMember("File").getAMethodCall(["open", "try_convert"])
|
||||
or
|
||||
// Calls to `Kernel.open` can yield `File` instances
|
||||
result.(KernelMethodCall).getMethodName() = "open" and
|
||||
// Assume that calls that don't invoke shell commands will instead open
|
||||
// a file.
|
||||
not pathArgSpawnsSubprocess(result.(KernelMethodCall).getArgument(0).asExpr().getExpr())
|
||||
}
|
||||
|
||||
private DataFlow::Node fileInstance() {
|
||||
result = fileInstanceInstantiation()
|
||||
or
|
||||
exists(DataFlow::Node inst |
|
||||
inst = fileInstance() and
|
||||
inst.(DataFlow::LocalSourceNode).flowsTo(result)
|
||||
)
|
||||
}
|
||||
|
||||
abstract private class IOOrFileMethodCall extends DataFlow::CallNode {
|
||||
// TODO: Currently this only handles class method calls.
|
||||
// Can we infer a path argument for instance method calls?
|
||||
// e.g. by tracing back to the instantiation of that instance
|
||||
DataFlow::Node getAPathArgumentImpl() {
|
||||
result = this.getArgument(0) and this.getReceiverKind() = "class"
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this call appears to read/write from/to a spawned subprocess,
|
||||
* rather than to/from a file.
|
||||
*/
|
||||
predicate spawnsSubprocess() {
|
||||
pathArgSpawnsSubprocess(this.getAPathArgumentImpl().asExpr().getExpr())
|
||||
}
|
||||
|
||||
/** Gets the API used to perform this call, either "IO" or "File" */
|
||||
abstract string getApi();
|
||||
|
||||
/** DEPRECATED: Alias for getApi */
|
||||
deprecated string getAPI() { result = this.getApi() }
|
||||
|
||||
/** Gets a node representing the data read or written by this call */
|
||||
abstract DataFlow::Node getADataNodeImpl();
|
||||
|
||||
/** Gets a string representation of the receiver kind, either "class" or "instance". */
|
||||
abstract string getReceiverKind();
|
||||
}
|
||||
|
||||
/**
|
||||
* A method call that performs a read using either the `IO` or `File` classes.
|
||||
*/
|
||||
private class IOOrFileReadMethodCall extends IOOrFileMethodCall {
|
||||
private string api;
|
||||
private string receiverKind;
|
||||
|
||||
IOOrFileReadMethodCall() {
|
||||
exists(string methodName | methodName = this.getMethodName() |
|
||||
// e.g. `{IO,File}.readlines("foo.txt")`
|
||||
receiverKind = "class" and
|
||||
methodName = ["binread", "foreach", "read", "readlines"] and
|
||||
api = ["IO", "File"] and
|
||||
this = API::getTopLevelMember(api).getAMethodCall(methodName)
|
||||
or
|
||||
// e.g. `{IO,File}.new("foo.txt", "r").getc`
|
||||
receiverKind = "interface" and
|
||||
(
|
||||
methodName =
|
||||
[
|
||||
"getbyte", "getc", "gets", "pread", "read", "read_nonblock", "readbyte", "readchar",
|
||||
"readline", "readlines", "readpartial", "sysread"
|
||||
] and
|
||||
(
|
||||
this.getReceiver() = ioInstance() and api = "IO"
|
||||
or
|
||||
this.getReceiver() = fileInstance() and api = "File"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override string getApi() { result = api }
|
||||
|
||||
/** DEPRECATED: Alias for getApi */
|
||||
deprecated override string getAPI() { result = this.getApi() }
|
||||
|
||||
override DataFlow::Node getADataNodeImpl() { result = this }
|
||||
|
||||
override string getReceiverKind() { result = receiverKind }
|
||||
}
|
||||
|
||||
/**
|
||||
* A method call that performs a write using either the `IO` or `File` classes.
|
||||
*/
|
||||
private class IOOrFileWriteMethodCall extends IOOrFileMethodCall {
|
||||
private string api;
|
||||
private string receiverKind;
|
||||
private DataFlow::Node dataNode;
|
||||
|
||||
IOOrFileWriteMethodCall() {
|
||||
exists(string methodName | methodName = this.getMethodName() |
|
||||
// e.g. `{IO,File}.write("foo.txt", "hello\n")`
|
||||
receiverKind = "class" and
|
||||
api = ["IO", "File"] and
|
||||
this = API::getTopLevelMember(api).getAMethodCall(methodName) and
|
||||
methodName = ["binwrite", "write"] and
|
||||
dataNode = this.getArgument(1)
|
||||
or
|
||||
// e.g. `{IO,File}.new("foo.txt", "a+).puts("hello")`
|
||||
receiverKind = "interface" and
|
||||
(
|
||||
this.getReceiver() = ioInstance() and api = "IO"
|
||||
or
|
||||
this.getReceiver() = fileInstance() and api = "File"
|
||||
) and
|
||||
(
|
||||
methodName = ["<<", "print", "putc", "puts", "syswrite", "pwrite", "write_nonblock"] and
|
||||
dataNode = this.getArgument(0)
|
||||
or
|
||||
// Any argument to these methods may be written as data
|
||||
methodName = ["printf", "write"] and dataNode = this.getArgument(_)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override string getApi() { result = api }
|
||||
|
||||
/** DEPRECATED: Alias for getApi */
|
||||
deprecated override string getAPI() { result = this.getApi() }
|
||||
|
||||
override DataFlow::Node getADataNodeImpl() { result = dataNode }
|
||||
|
||||
override string getReceiverKind() { result = receiverKind }
|
||||
}
|
||||
|
||||
/**
|
||||
* Classes and predicates for modeling the core `IO` module.
|
||||
*/
|
||||
module IO {
|
||||
/**
|
||||
* An instance of the `IO` class, for example in
|
||||
*
|
||||
* ```rb
|
||||
* rand = IO.new(IO.sysopen("/dev/random", "r"), "r")
|
||||
* rand_data = rand.read(32)
|
||||
* ```
|
||||
*
|
||||
* there are 3 `IOInstance`s - the call to `IO.new`, the assignment
|
||||
* `rand = ...`, and the read access to `rand` on the second line.
|
||||
*/
|
||||
class IOInstance extends DataFlow::Node {
|
||||
IOInstance() {
|
||||
this = ioInstance() or
|
||||
this = fileInstance()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A `DataFlow::CallNode` that reads data using the `IO` class. For example,
|
||||
* the `read` and `readline` calls in:
|
||||
*
|
||||
* ```rb
|
||||
* # invokes the `date` shell command as a subprocess, returning its output as a string
|
||||
* IO.read("|date")
|
||||
*
|
||||
* # reads from the file `foo.txt`, returning its first line as a string
|
||||
* IO.new(IO.sysopen("foo.txt")).readline
|
||||
* ```
|
||||
*
|
||||
* This class includes only reads that use the `IO` class directly, not those
|
||||
* that use a subclass of `IO` such as `File`.
|
||||
*/
|
||||
class IOReader extends IOOrFileReadMethodCall {
|
||||
IOReader() { this.getApi() = "IO" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A `DataFlow::CallNode` that writes data using the `IO` class. For example,
|
||||
* the `write` and `puts` calls in:
|
||||
*
|
||||
* ```rb
|
||||
* # writes the string `hello world` to the file `foo.txt`
|
||||
* IO.write("foo.txt", "hello world")
|
||||
*
|
||||
* # appends the string `hello again\n` to the file `foo.txt`
|
||||
* IO.new(IO.sysopen("foo.txt", "a")).puts("hello again")
|
||||
* ```
|
||||
*
|
||||
* This class includes only writes that use the `IO` class directly, not those
|
||||
* that use a subclass of `IO` such as `File`.
|
||||
*/
|
||||
class IOWriter extends IOOrFileWriteMethodCall {
|
||||
IOWriter() { this.getApi() = "IO" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A `DataFlow::CallNode` that reads data to the filesystem using the `IO`
|
||||
* or `File` classes. For example, the `IO.read` and `File#readline` calls in:
|
||||
*
|
||||
* ```rb
|
||||
* # reads the file `foo.txt` and returns its contents as a string.
|
||||
* IO.read("foo.txt")
|
||||
*
|
||||
* # reads from the file `foo.txt`, returning its first line as a string
|
||||
* File.new("foo.txt").readline
|
||||
* ```
|
||||
*/
|
||||
class FileReader extends IOOrFileReadMethodCall, FileSystemReadAccess::Range {
|
||||
FileReader() { not this.spawnsSubprocess() }
|
||||
|
||||
override DataFlow::Node getADataNode() { result = this.getADataNodeImpl() }
|
||||
|
||||
override DataFlow::Node getAPathArgument() { result = this.getAPathArgumentImpl() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A `DataFlow::CallNode` that reads data from the filesystem using the `IO`
|
||||
* or `File` classes. For example, the `write` and `puts` calls in:
|
||||
*
|
||||
* ```rb
|
||||
* # writes the string `hello world` to the file `foo.txt`
|
||||
* IO.write("foo.txt", "hello world")
|
||||
*
|
||||
* # appends the string `hello again\n` to the file `foo.txt`
|
||||
* File.new("foo.txt", "a").puts("hello again")
|
||||
* ```
|
||||
*/
|
||||
class FileWriter extends IOOrFileWriteMethodCall, FileSystemWriteAccess::Range {
|
||||
FileWriter() { not this.spawnsSubprocess() }
|
||||
|
||||
override DataFlow::Node getADataNode() { result = this.getADataNodeImpl() }
|
||||
|
||||
override DataFlow::Node getAPathArgument() { result = this.getAPathArgumentImpl() }
|
||||
}
|
||||
}
|
||||
private import core.IO
|
||||
private import core.Kernel::Kernel
|
||||
private import core.internal.IOOrFile
|
||||
|
||||
/**
|
||||
* Classes and predicates for modeling the core `File` module.
|
||||
@@ -330,7 +73,7 @@ module File {
|
||||
])
|
||||
or
|
||||
// Instance methods
|
||||
exists(FileInstance fi |
|
||||
exists(File::FileInstance fi |
|
||||
this.getReceiver() = fi and
|
||||
this.getMethodName() = ["path", "to_path"]
|
||||
)
|
||||
|
||||
153
ruby/ql/lib/codeql/ruby/frameworks/core/IO.qll
Normal file
153
ruby/ql/lib/codeql/ruby/frameworks/core/IO.qll
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Provides modeling for the `IO` module.
|
||||
*/
|
||||
|
||||
private import codeql.ruby.ApiGraphs
|
||||
private import codeql.ruby.Concepts
|
||||
private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.controlflow.CfgNodes
|
||||
private import codeql.ruby.frameworks.Files as Files
|
||||
private import internal.IOOrFile
|
||||
|
||||
/** Provides modeling for the `IO` class. */
|
||||
module IO {
|
||||
/**
|
||||
* An instance of the `IO` class, for example in
|
||||
*
|
||||
* ```rb
|
||||
* rand = IO.new(IO.sysopen("/dev/random", "r"), "r")
|
||||
* rand_data = rand.read(32)
|
||||
* ```
|
||||
*
|
||||
* there are 3 `IOInstance`s - the call to `IO.new`, the assignment
|
||||
* `rand = ...`, and the read access to `rand` on the second line.
|
||||
*/
|
||||
class IOInstance extends DataFlow::Node {
|
||||
IOInstance() { this = [ioInstance(), fileInstance()] }
|
||||
}
|
||||
|
||||
/**
|
||||
* A `DataFlow::CallNode` that reads data using the `IO` class. For example,
|
||||
* the `read` and `readline` calls in:
|
||||
*
|
||||
* ```rb
|
||||
* # invokes the `date` shell command as a subprocess, returning its output as a string
|
||||
* IO.read("|date")
|
||||
*
|
||||
* # reads from the file `foo.txt`, returning its first line as a string
|
||||
* IO.new(IO.sysopen("foo.txt")).readline
|
||||
* ```
|
||||
*
|
||||
* This class includes only reads that use the `IO` class directly, not those
|
||||
* that use a subclass of `IO` such as `File`.
|
||||
*/
|
||||
class IOReader = Readers::IOReader;
|
||||
|
||||
/**
|
||||
* A `DataFlow::CallNode` that writes data using the `IO` class. For example,
|
||||
* the `write` and `puts` calls in:
|
||||
*
|
||||
* ```rb
|
||||
* # writes the string `hello world` to the file `foo.txt`
|
||||
* IO.write("foo.txt", "hello world")
|
||||
*
|
||||
* # appends the string `hello again\n` to the file `foo.txt`
|
||||
* IO.new(IO.sysopen("foo.txt", "a")).puts("hello again")
|
||||
* ```
|
||||
*
|
||||
* This class includes only writes that use the `IO` class directly, not those
|
||||
* that use a subclass of `IO` such as `File`.
|
||||
*/
|
||||
class IOWriter = Writers::IOWriter;
|
||||
|
||||
/**
|
||||
* A `DataFlow::CallNode` that reads data to the filesystem using the `IO`
|
||||
* or `File` classes. For example, the `IO.read` and `File#readline` calls in:
|
||||
*
|
||||
* ```rb
|
||||
* # reads the file `foo.txt` and returns its contents as a string.
|
||||
* IO.read("foo.txt")
|
||||
*
|
||||
* # reads from the file `foo.txt`, returning its first line as a string
|
||||
* File.new("foo.txt").readline
|
||||
* ```
|
||||
*/
|
||||
class FileReader = Readers::FileReader;
|
||||
|
||||
/**
|
||||
* A `DataFlow::CallNode` that reads data from the filesystem using the `IO`
|
||||
* or `File` classes. For example, the `write` and `puts` calls in:
|
||||
*
|
||||
* ```rb
|
||||
* # writes the string `hello world` to the file `foo.txt`
|
||||
* IO.write("foo.txt", "hello world")
|
||||
*
|
||||
* # appends the string `hello again\n` to the file `foo.txt`
|
||||
* File.new("foo.txt", "a").puts("hello again")
|
||||
* ```
|
||||
*/
|
||||
class FileWriter = Writers::FileWriter;
|
||||
|
||||
/**
|
||||
* A system command executed via the `IO.popen` method.
|
||||
* Signature:
|
||||
* ```
|
||||
* popen([env,] cmd, mode="r" [, opt]) -> io
|
||||
* popen([env,] cmd, mode="r" [, opt]) {|io| block } -> obj
|
||||
* ```
|
||||
* `IO.popen` does different things based on the the value of `cmd`:
|
||||
* ```
|
||||
* "-" : fork
|
||||
* commandline : command line string which is passed to a shell
|
||||
* [env, cmdname, arg1, ..., opts] : command name and zero or more arguments (no shell)
|
||||
* [env, [cmdname, argv0], arg1, ..., opts] : command name, argv[0] and zero or more arguments (no shell)
|
||||
* (env and opts are optional.)
|
||||
* ```
|
||||
* Examples:
|
||||
* ```ruby
|
||||
* IO.popen("cat foo.txt | tail")
|
||||
* IO.popen({some_env_var: "123"}, "cat foo.txt | tail")
|
||||
* IO.popen(["cat", "foo.txt"])
|
||||
* IO.popen([{some_env_var: "123"}, "cat", "foo.txt"])
|
||||
* IO.popen([["cat", "argv0"], "foo.txt"])
|
||||
* IO.popen([{some_env_var: "123"}, ["cat", "argv0"], "foo.txt"])
|
||||
* ```
|
||||
* Ruby documentation: https://docs.ruby-lang.org/en/3.1/IO.html#method-c-popen
|
||||
*/
|
||||
class POpenCall extends SystemCommandExecution::Range, DataFlow::CallNode {
|
||||
POpenCall() { this = API::getTopLevelMember("IO").getAMethodCall("popen") }
|
||||
|
||||
override DataFlow::Node getAnArgument() { this.argument(result, _) }
|
||||
|
||||
override predicate isShellInterpreted(DataFlow::Node arg) { this.argument(arg, true) }
|
||||
|
||||
/**
|
||||
* Holds if `arg` is an argument to this call. `shell` is true if the argument is passed to a subshell.
|
||||
*/
|
||||
private predicate argument(DataFlow::Node arg, boolean shell) {
|
||||
exists(ExprCfgNode n | n = arg.asExpr() |
|
||||
// Exclude any hash literal arguments, which are likely to be environment variables or options.
|
||||
not n instanceof ExprNodes::HashLiteralCfgNode and
|
||||
not n instanceof ExprNodes::ArrayLiteralCfgNode and
|
||||
(
|
||||
// IO.popen({var: "a"}, "cmd", {some: :opt})
|
||||
arg = this.getArgument([0, 1]) and
|
||||
// We over-approximate by assuming a subshell if the argument isn't an array or "-".
|
||||
// This increases the sensitivity of the CommandInjection query at the risk of some FPs.
|
||||
if n.getConstantValue().getString() = "-" then shell = false else shell = true
|
||||
or
|
||||
// IO.popen([{var: "b"}, "cmd", "arg1", "arg2", {some: :opt}])
|
||||
// IO.popen({var: "a"}, ["cmd", "arg1", "arg2", {some: :opt}])
|
||||
shell = false and
|
||||
exists(ExprNodes::ArrayLiteralCfgNode arr | this.getArgument([0, 1]).asExpr() = arr |
|
||||
n = arr.getAnArgument()
|
||||
or
|
||||
// IO.popen([{var: "b"}, ["cmd", "argv0"], "arg1", "arg2", {some: :opt}])
|
||||
// IO.popen([["cmd", "argv0"], "arg1", "arg2", {some: :opt}])
|
||||
n = arr.getArgument([0, 1]).(ExprNodes::ArrayLiteralCfgNode).getArgument(0)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
250
ruby/ql/lib/codeql/ruby/frameworks/core/internal/IOOrFile.qll
Normal file
250
ruby/ql/lib/codeql/ruby/frameworks/core/internal/IOOrFile.qll
Normal file
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Provides modeling for concepts shared across `File` and `IO`.
|
||||
*/
|
||||
|
||||
private import ruby
|
||||
private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.ApiGraphs
|
||||
private import codeql.ruby.frameworks.Files
|
||||
private import codeql.ruby.frameworks.core.Kernel::Kernel
|
||||
private import codeql.ruby.Concepts
|
||||
|
||||
DataFlow::Node ioInstanceInstantiation() {
|
||||
result = API::getTopLevelMember("IO").getAnInstantiation() or
|
||||
result = API::getTopLevelMember("IO").getAMethodCall(["for_fd", "open", "try_convert"])
|
||||
}
|
||||
|
||||
DataFlow::Node ioInstance() {
|
||||
result = ioInstanceInstantiation()
|
||||
or
|
||||
exists(DataFlow::Node inst |
|
||||
inst = ioInstance() and
|
||||
inst.(DataFlow::LocalSourceNode).flowsTo(result)
|
||||
)
|
||||
}
|
||||
|
||||
DataFlow::Node fileInstanceInstantiation() {
|
||||
result = API::getTopLevelMember("File").getAnInstantiation()
|
||||
or
|
||||
result = API::getTopLevelMember("File").getAMethodCall(["open", "try_convert"])
|
||||
or
|
||||
// Calls to `Kernel.open` can yield `File` instances
|
||||
result.(KernelMethodCall).getMethodName() = "open" and
|
||||
// Assume that calls that don't invoke shell commands will instead open
|
||||
// a file.
|
||||
not pathArgSpawnsSubprocess(result.(KernelMethodCall).getArgument(0).asExpr().getExpr())
|
||||
}
|
||||
|
||||
DataFlow::Node fileInstance() {
|
||||
result = fileInstanceInstantiation()
|
||||
or
|
||||
exists(DataFlow::Node inst |
|
||||
inst = fileInstance() and
|
||||
inst.(DataFlow::LocalSourceNode).flowsTo(result)
|
||||
)
|
||||
}
|
||||
|
||||
abstract class IOOrFileMethodCall extends DataFlow::CallNode {
|
||||
// TODO: Currently this only handles class method calls.
|
||||
// Can we infer a path argument for instance method calls?
|
||||
// e.g. by tracing back to the instantiation of that instance
|
||||
DataFlow::Node getAPathArgumentImpl() {
|
||||
result = this.getArgument(0) and this.getReceiverKind() = "class"
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this call appears to read/write from/to a spawned subprocess,
|
||||
* rather than to/from a file.
|
||||
*/
|
||||
predicate spawnsSubprocess() {
|
||||
pathArgSpawnsSubprocess(this.getAPathArgumentImpl().asExpr().getExpr())
|
||||
}
|
||||
|
||||
/** Gets the API used to perform this call, either "IO" or "File" */
|
||||
abstract string getApi();
|
||||
|
||||
/** DEPRECATED: Alias for getApi */
|
||||
deprecated string getAPI() { result = this.getApi() }
|
||||
|
||||
/** Gets a node representing the data read or written by this call */
|
||||
abstract DataFlow::Node getADataNodeImpl();
|
||||
|
||||
/** Gets a string representation of the receiver kind, either "class" or "instance". */
|
||||
abstract string getReceiverKind();
|
||||
}
|
||||
|
||||
// Match some simple cases where a path argument specifies a shell command to
|
||||
// be executed. For example, the `"|date"` argument in `IO.read("|date")`, which
|
||||
// will execute a shell command and read its output rather than reading from the
|
||||
// filesystem.
|
||||
predicate pathArgSpawnsSubprocess(Expr arg) {
|
||||
arg.getConstantValue().getStringlikeValue().charAt(0) = "|"
|
||||
}
|
||||
|
||||
/**
|
||||
* A method call that performs a read using either the `IO` or `File` classes.
|
||||
*/
|
||||
class IOOrFileReadMethodCall extends IOOrFileMethodCall {
|
||||
private string api;
|
||||
private string receiverKind;
|
||||
|
||||
IOOrFileReadMethodCall() {
|
||||
exists(string methodName | methodName = this.getMethodName() |
|
||||
// e.g. `{IO,File}.readlines("foo.txt")`
|
||||
receiverKind = "class" and
|
||||
methodName = ["binread", "foreach", "read", "readlines"] and
|
||||
api = ["IO", "File"] and
|
||||
this = API::getTopLevelMember(api).getAMethodCall(methodName)
|
||||
or
|
||||
// e.g. `{IO,File}.new("foo.txt", "r").getc`
|
||||
receiverKind = "interface" and
|
||||
(
|
||||
methodName =
|
||||
[
|
||||
"getbyte", "getc", "gets", "pread", "read", "read_nonblock", "readbyte", "readchar",
|
||||
"readline", "readlines", "readpartial", "sysread"
|
||||
] and
|
||||
(
|
||||
this.getReceiver() = fileInstance() and api = "File"
|
||||
or
|
||||
this.getReceiver() = ioInstance() and api = "IO"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override string getApi() { result = api }
|
||||
|
||||
/** DEPRECATED: Alias for getApi */
|
||||
deprecated override string getAPI() { result = this.getApi() }
|
||||
|
||||
override DataFlow::Node getADataNodeImpl() { result = this }
|
||||
|
||||
override string getReceiverKind() { result = receiverKind }
|
||||
}
|
||||
|
||||
/**
|
||||
* A method call that performs a write using either the `IO` or `File` classes.
|
||||
*/
|
||||
class IOOrFileWriteMethodCall extends IOOrFileMethodCall {
|
||||
private string api;
|
||||
private string receiverKind;
|
||||
private DataFlow::Node dataNode;
|
||||
|
||||
IOOrFileWriteMethodCall() {
|
||||
exists(string methodName | methodName = this.getMethodName() |
|
||||
// e.g. `{IO,File}.write("foo.txt", "hello\n")`
|
||||
receiverKind = "class" and
|
||||
api = ["IO", "File"] and
|
||||
this = API::getTopLevelMember(api).getAMethodCall(methodName) and
|
||||
methodName = ["binwrite", "write"] and
|
||||
dataNode = this.getArgument(1)
|
||||
or
|
||||
// e.g. `{IO,File}.new("foo.txt", "a+).puts("hello")`
|
||||
receiverKind = "interface" and
|
||||
(
|
||||
this.getReceiver() = fileInstance() and api = "File"
|
||||
or
|
||||
this.getReceiver() = ioInstance() and api = "IO"
|
||||
) and
|
||||
(
|
||||
methodName = ["<<", "print", "putc", "puts", "syswrite", "pwrite", "write_nonblock"] and
|
||||
dataNode = this.getArgument(0)
|
||||
or
|
||||
// Any argument to these methods may be written as data
|
||||
methodName = ["printf", "write"] and dataNode = this.getArgument(_)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override string getApi() { result = api }
|
||||
|
||||
/** DEPRECATED: Alias for getApi */
|
||||
deprecated override string getAPI() { result = this.getApi() }
|
||||
|
||||
override DataFlow::Node getADataNodeImpl() { result = dataNode }
|
||||
|
||||
override string getReceiverKind() { result = receiverKind }
|
||||
}
|
||||
|
||||
module Readers {
|
||||
/**
|
||||
* A `DataFlow::CallNode` that reads data using the `IO` class. For example,
|
||||
* the `read` and `readline` calls in:
|
||||
*
|
||||
* ```rb
|
||||
* # invokes the `date` shell command as a subprocess, returning its output as a string
|
||||
* IO.read("|date")
|
||||
*
|
||||
* # reads from the file `foo.txt`, returning its first line as a string
|
||||
* IO.new(IO.sysopen("foo.txt")).readline
|
||||
* ```
|
||||
*
|
||||
* This class includes only reads that use the `IO` class directly, not those
|
||||
* that use a subclass of `IO` such as `File`.
|
||||
*/
|
||||
class IOReader extends IOOrFileReadMethodCall {
|
||||
IOReader() { this.getApi() = "IO" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A `DataFlow::CallNode` that reads data to the filesystem using the `IO`
|
||||
* or `File` classes. For example, the `IO.read` and `File#readline` calls in:
|
||||
*
|
||||
* ```rb
|
||||
* # reads the file `foo.txt` and returns its contents as a string.
|
||||
* IO.read("foo.txt")
|
||||
*
|
||||
* # reads from the file `foo.txt`, returning its first line as a string
|
||||
* File.new("foo.txt").readline
|
||||
* ```
|
||||
*/
|
||||
class FileReader extends IOOrFileReadMethodCall, FileSystemReadAccess::Range {
|
||||
FileReader() { not this.spawnsSubprocess() }
|
||||
|
||||
override DataFlow::Node getADataNode() { result = this.getADataNodeImpl() }
|
||||
|
||||
override DataFlow::Node getAPathArgument() { result = this.getAPathArgumentImpl() }
|
||||
}
|
||||
}
|
||||
|
||||
module Writers {
|
||||
/**
|
||||
* A `DataFlow::CallNode` that writes data using the `IO` class. For example,
|
||||
* the `write` and `puts` calls in:
|
||||
*
|
||||
* ```rb
|
||||
* # writes the string `hello world` to the file `foo.txt`
|
||||
* IO.write("foo.txt", "hello world")
|
||||
*
|
||||
* # appends the string `hello again\n` to the file `foo.txt`
|
||||
* IO.new(IO.sysopen("foo.txt", "a")).puts("hello again")
|
||||
* ```
|
||||
*
|
||||
* This class includes only writes that use the `IO` class directly, not those
|
||||
* that use a subclass of `IO` such as `File`.
|
||||
*/
|
||||
class IOWriter extends IOOrFileWriteMethodCall {
|
||||
IOWriter() { this.getApi() = "IO" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A `DataFlow::CallNode` that reads data from the filesystem using the `IO`
|
||||
* or `File` classes. For example, the `write` and `puts` calls in:
|
||||
*
|
||||
* ```rb
|
||||
* # writes the string `hello world` to the file `foo.txt`
|
||||
* IO.write("foo.txt", "hello world")
|
||||
*
|
||||
* # appends the string `hello again\n` to the file `foo.txt`
|
||||
* File.new("foo.txt", "a").puts("hello again")
|
||||
* ```
|
||||
*/
|
||||
class FileWriter extends IOOrFileWriteMethodCall, FileSystemWriteAccess::Range {
|
||||
FileWriter() { not this.spawnsSubprocess() }
|
||||
|
||||
override DataFlow::Node getADataNode() { result = this.getADataNodeImpl() }
|
||||
|
||||
override DataFlow::Node getAPathArgument() { result = this.getAPathArgumentImpl() }
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.Concepts
|
||||
private import codeql.ruby.typetracking.TypeTracker
|
||||
private import codeql.ruby.frameworks.Files
|
||||
private import codeql.ruby.frameworks.core.IO
|
||||
|
||||
/**
|
||||
* Classes and predicates for reasoning about download of sensitive file through insecure connection vulnerabilities.
|
||||
|
||||
@@ -7,7 +7,8 @@ import codeql.ruby.DataFlow
|
||||
import codeql.ruby.dataflow.RemoteFlowSources
|
||||
import codeql.ruby.ApiGraphs
|
||||
import codeql.ruby.TaintTracking
|
||||
private import codeql.ruby.frameworks.Files::IO
|
||||
private import codeql.ruby.frameworks.Files
|
||||
private import codeql.ruby.frameworks.core.IO
|
||||
private import codeql.ruby.controlflow.CfgNodes
|
||||
|
||||
/**
|
||||
@@ -67,5 +68,7 @@ class KernelSprintfCall extends PrintfStyleCall {
|
||||
* A call to `IO#printf`.
|
||||
*/
|
||||
class IOPrintfCall extends PrintfStyleCall {
|
||||
IOPrintfCall() { this.getReceiver() instanceof IOInstance and this.getMethodName() = "printf" }
|
||||
IOPrintfCall() {
|
||||
this.getReceiver() instanceof IO::IOInstance and this.getMethodName() = "printf"
|
||||
}
|
||||
}
|
||||
|
||||
55
ruby/ql/test/library-tests/frameworks/core/IO.expected
Normal file
55
ruby/ql/test/library-tests/frameworks/core/IO.expected
Normal file
@@ -0,0 +1,55 @@
|
||||
ioPOpenCalls
|
||||
| IO.rb:1:1:1:30 | call to popen |
|
||||
| IO.rb:2:1:2:53 | call to popen |
|
||||
| IO.rb:3:1:3:67 | call to popen |
|
||||
| IO.rb:5:1:5:28 | call to popen |
|
||||
| IO.rb:6:1:6:51 | call to popen |
|
||||
| IO.rb:7:1:7:65 | call to popen |
|
||||
| IO.rb:9:1:9:39 | call to popen |
|
||||
| IO.rb:10:1:10:62 | call to popen |
|
||||
| IO.rb:11:1:11:76 | call to popen |
|
||||
| IO.rb:12:1:12:76 | call to popen |
|
||||
| IO.rb:14:1:14:13 | call to popen |
|
||||
| IO.rb:15:1:15:36 | call to popen |
|
||||
| IO.rb:16:1:16:50 | call to popen |
|
||||
| IO.rb:19:1:19:13 | call to popen |
|
||||
| IO.rb:20:1:20:36 | call to popen |
|
||||
| IO.rb:21:1:21:50 | call to popen |
|
||||
| IO.rb:24:1:24:13 | call to popen |
|
||||
| IO.rb:25:1:25:36 | call to popen |
|
||||
| IO.rb:26:1:26:50 | call to popen |
|
||||
| IO.rb:29:1:29:13 | call to popen |
|
||||
| IO.rb:30:1:30:36 | call to popen |
|
||||
| IO.rb:31:1:31:50 | call to popen |
|
||||
| IO.rb:34:3:34:15 | call to popen |
|
||||
ioPOpenCallArguments
|
||||
| IO.rb:1:1:1:30 | call to popen | true | IO.rb:1:10:1:29 | "cat foo.txt \| tail" |
|
||||
| IO.rb:2:1:2:53 | call to popen | true | IO.rb:2:33:2:52 | "cat foo.txt \| tail" |
|
||||
| IO.rb:3:1:3:67 | call to popen | true | IO.rb:3:33:3:52 | "cat foo.txt \| tail" |
|
||||
| IO.rb:5:1:5:28 | call to popen | false | IO.rb:5:11:5:15 | "cat" |
|
||||
| IO.rb:5:1:5:28 | call to popen | false | IO.rb:5:18:5:26 | "foo.txt" |
|
||||
| IO.rb:6:1:6:51 | call to popen | false | IO.rb:6:34:6:38 | "cat" |
|
||||
| IO.rb:6:1:6:51 | call to popen | false | IO.rb:6:41:6:49 | "foo.txt" |
|
||||
| IO.rb:7:1:7:65 | call to popen | false | IO.rb:7:34:7:38 | "cat" |
|
||||
| IO.rb:7:1:7:65 | call to popen | false | IO.rb:7:41:7:49 | "foo.txt" |
|
||||
| IO.rb:9:1:9:39 | call to popen | false | IO.rb:9:12:9:16 | "cat" |
|
||||
| IO.rb:9:1:9:39 | call to popen | false | IO.rb:9:29:9:37 | "foo.txt" |
|
||||
| IO.rb:10:1:10:62 | call to popen | false | IO.rb:10:35:10:39 | "cat" |
|
||||
| IO.rb:10:1:10:62 | call to popen | false | IO.rb:10:52:10:60 | "foo.txt" |
|
||||
| IO.rb:11:1:11:76 | call to popen | false | IO.rb:11:35:11:39 | "cat" |
|
||||
| IO.rb:11:1:11:76 | call to popen | false | IO.rb:11:52:11:60 | "foo.txt" |
|
||||
| IO.rb:12:1:12:76 | call to popen | false | IO.rb:12:35:12:39 | "cat" |
|
||||
| IO.rb:12:1:12:76 | call to popen | false | IO.rb:12:52:12:60 | "foo.txt" |
|
||||
| IO.rb:14:1:14:13 | call to popen | false | IO.rb:14:10:14:12 | "-" |
|
||||
| IO.rb:15:1:15:36 | call to popen | false | IO.rb:15:33:15:35 | "-" |
|
||||
| IO.rb:16:1:16:50 | call to popen | false | IO.rb:16:33:16:35 | "-" |
|
||||
| IO.rb:19:1:19:13 | call to popen | true | IO.rb:19:10:19:12 | cmd |
|
||||
| IO.rb:20:1:20:36 | call to popen | true | IO.rb:20:33:20:35 | cmd |
|
||||
| IO.rb:21:1:21:50 | call to popen | true | IO.rb:21:33:21:35 | cmd |
|
||||
| IO.rb:24:1:24:13 | call to popen | true | IO.rb:24:10:24:12 | cmd |
|
||||
| IO.rb:25:1:25:36 | call to popen | true | IO.rb:25:33:25:35 | cmd |
|
||||
| IO.rb:26:1:26:50 | call to popen | true | IO.rb:26:33:26:35 | cmd |
|
||||
| IO.rb:29:1:29:13 | call to popen | true | IO.rb:29:10:29:12 | cmd |
|
||||
| IO.rb:30:1:30:36 | call to popen | true | IO.rb:30:33:30:35 | cmd |
|
||||
| IO.rb:31:1:31:50 | call to popen | true | IO.rb:31:33:31:35 | cmd |
|
||||
| IO.rb:34:3:34:15 | call to popen | true | IO.rb:34:12:34:14 | cmd |
|
||||
9
ruby/ql/test/library-tests/frameworks/core/IO.ql
Normal file
9
ruby/ql/test/library-tests/frameworks/core/IO.ql
Normal file
@@ -0,0 +1,9 @@
|
||||
import codeql.ruby.frameworks.core.IO::IO
|
||||
import codeql.ruby.DataFlow
|
||||
|
||||
query predicate ioPOpenCalls(POpenCall c) { any() }
|
||||
|
||||
query DataFlow::Node ioPOpenCallArguments(POpenCall c, boolean shellInterpreted) {
|
||||
result = c.getAnArgument() and
|
||||
if c.isShellInterpreted(result) then shellInterpreted = true else shellInterpreted = false
|
||||
}
|
||||
38
ruby/ql/test/library-tests/frameworks/core/IO.rb
Normal file
38
ruby/ql/test/library-tests/frameworks/core/IO.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
IO.popen("cat foo.txt | tail")
|
||||
IO.popen({some_env_var: "123"}, "cat foo.txt | tail")
|
||||
IO.popen({some_env_var: "123"}, "cat foo.txt | tail", {some: :opt})
|
||||
|
||||
IO.popen(["cat", "foo.txt"])
|
||||
IO.popen([{some_env_var: "123"}, "cat", "foo.txt"])
|
||||
IO.popen([{some_env_var: "123"}, "cat", "foo.txt"], {some: :opt})
|
||||
|
||||
IO.popen([["cat", "argv0"], "foo.txt"])
|
||||
IO.popen([{some_env_var: "123"}, ["cat", "argv0"], "foo.txt"])
|
||||
IO.popen([{some_env_var: "123"}, ["cat", "argv0"], "foo.txt"], {some: :opt})
|
||||
IO.popen({some_env_var: "123"}, [["cat", "argv0"], "foo.txt"], {some: :opt})
|
||||
|
||||
IO.popen("-")
|
||||
IO.popen({some_env_var: "123"}, "-")
|
||||
IO.popen({some_env_var: "123"}, "-", {some: :opt})
|
||||
|
||||
cmd = "cat foo.txt | tail"
|
||||
IO.popen(cmd)
|
||||
IO.popen({some_env_var: "123"}, cmd)
|
||||
IO.popen({some_env_var: "123"}, cmd, {some: :opt})
|
||||
|
||||
cmd = ["cat", "foo.txt"]
|
||||
IO.popen(cmd)
|
||||
IO.popen({some_env_var: "123"}, cmd)
|
||||
IO.popen({some_env_var: "123"}, cmd, {some: :opt})
|
||||
|
||||
cmd = [["cat", "argv0"], "foo.txt"]
|
||||
IO.popen(cmd)
|
||||
IO.popen({some_env_var: "123"}, cmd)
|
||||
IO.popen({some_env_var: "123"}, cmd, {some: :opt})
|
||||
|
||||
def popen(cmd)
|
||||
IO.popen(cmd)
|
||||
end
|
||||
|
||||
popen("cat foo.txt | tail")
|
||||
popen(["cat", "foo.txt"])
|
||||
@@ -1,5 +1,6 @@
|
||||
private import ruby
|
||||
private import codeql.ruby.frameworks.Files
|
||||
private import codeql.ruby.frameworks.core.IO
|
||||
private import codeql.ruby.Concepts
|
||||
|
||||
query predicate fileInstances(File::FileInstance i) { any() }
|
||||
|
||||
Reference in New Issue
Block a user