mirror of
https://github.com/github/codeql.git
synced 2026-05-01 03:35:13 +02:00
Ruby: Model IO.popen
This method is very similar to `Kernel.system`: it executes its arguments as a system command in various ways.
This commit is contained in:
@@ -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.
|
||||
|
||||
71
ruby/ql/lib/codeql/ruby/frameworks/core/IO.qll
Normal file
71
ruby/ql/lib/codeql/ruby/frameworks/core/IO.qll
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
/** Provides modeling for the `IO` class. */
|
||||
module IO {
|
||||
/**
|
||||
* 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.)
|
||||
* ```
|
||||
* ```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.0/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) }
|
||||
|
||||
/**
|
||||
* A helper predicate that 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: "a"}, [{var: "b"}, "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: "a"}, [{var: "b"}, ["cmd", "argv0"], "arg1", "arg2", {some: :opt}])
|
||||
n = arr.getArgument(0).(ExprNodes::ArrayLiteralCfgNode).getArgument(0)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
50
ruby/ql/test/library-tests/frameworks/core/IO.expected
Normal file
50
ruby/ql/test/library-tests/frameworks/core/IO.expected
Normal file
@@ -0,0 +1,50 @@
|
||||
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:13:1:13:13 | call to popen |
|
||||
| IO.rb:14:1:14:36 | call to popen |
|
||||
| IO.rb:15:1:15:50 | call to popen |
|
||||
| IO.rb:18:1:18:13 | call to popen |
|
||||
| IO.rb:19:1:19:36 | call to popen |
|
||||
| IO.rb:20:1:20:50 | call to popen |
|
||||
| IO.rb:23:1:23:13 | call to popen |
|
||||
| IO.rb:24:1:24:36 | call to popen |
|
||||
| IO.rb:25:1:25:50 | call to popen |
|
||||
| IO.rb:28:1:28:13 | call to popen |
|
||||
| IO.rb:29:1:29:36 | call to popen |
|
||||
| IO.rb:30:1:30:50 | call to popen |
|
||||
| IO.rb:33:3:33: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:52:10:60 | "foo.txt" |
|
||||
| IO.rb:11:1:11:76 | call to popen | false | IO.rb:11:52:11:60 | "foo.txt" |
|
||||
| IO.rb:13:1:13:13 | call to popen | false | IO.rb:13:10:13:12 | "-" |
|
||||
| IO.rb:14:1:14:36 | call to popen | false | IO.rb:14:33:14:35 | "-" |
|
||||
| IO.rb:15:1:15:50 | call to popen | false | IO.rb:15:33:15:35 | "-" |
|
||||
| IO.rb:18:1:18:13 | call to popen | true | IO.rb:18:10:18:12 | cmd |
|
||||
| IO.rb:19:1:19:36 | call to popen | true | IO.rb:19:33:19:35 | cmd |
|
||||
| IO.rb:20:1:20:50 | call to popen | true | IO.rb:20:33:20:35 | cmd |
|
||||
| IO.rb:23:1:23:13 | call to popen | true | IO.rb:23:10:23:12 | cmd |
|
||||
| IO.rb:24:1:24:36 | call to popen | true | IO.rb:24:33:24:35 | cmd |
|
||||
| IO.rb:25:1:25:50 | call to popen | true | IO.rb:25:33:25:35 | cmd |
|
||||
| IO.rb:28:1:28:13 | call to popen | true | IO.rb:28:10:28:12 | cmd |
|
||||
| IO.rb:29:1:29:36 | call to popen | true | IO.rb:29:33:29:35 | cmd |
|
||||
| IO.rb:30:1:30:50 | call to popen | true | IO.rb:30:33:30:35 | cmd |
|
||||
| IO.rb:33:3:33:15 | call to popen | true | IO.rb:33:12:33: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
|
||||
}
|
||||
37
ruby/ql/test/library-tests/frameworks/core/IO.rb
Normal file
37
ruby/ql/test/library-tests/frameworks/core/IO.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
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("-")
|
||||
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"])
|
||||
Reference in New Issue
Block a user