Skip to content

Commit a8f0bce

Browse files
committed
Add SystemCommandExecution concept
A SystemCommandExecution is a method call or builtin that executes a system command, either directly or via a subshell.
1 parent 1fd91ab commit a8f0bce

File tree

6 files changed

+337
-0
lines changed

6 files changed

+337
-0
lines changed

ql/lib/codeql/ruby/Concepts.qll

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ private import codeql.ruby.CFG
99
private import codeql.ruby.DataFlow
1010
private import codeql.ruby.Frameworks
1111
private import codeql.ruby.dataflow.RemoteFlowSources
12+
private import codeql.ruby.ApiGraphs
1213

1314
/**
1415
* A data-flow node that executes SQL statements.
@@ -312,3 +313,36 @@ module HTTP {
312313
}
313314
}
314315
}
316+
317+
/**
318+
* A data flow node that executes an operating system command,
319+
* for instance by spawning a new process.
320+
*/
321+
class SystemCommandExecution extends DataFlow::Node {
322+
SystemCommandExecution::Range range;
323+
324+
SystemCommandExecution() { this = range }
325+
326+
/** Holds if a shell interprets `arg`. */
327+
predicate isShellInterpreted(DataFlow::Node arg) { range.isShellInterpreted(arg) }
328+
329+
/** Gets an argument to this execution that specifies the command or an argument to it. */
330+
DataFlow::Node getAnArgument() { result = range.getAnArgument() }
331+
}
332+
333+
module SystemCommandExecution {
334+
/**
335+
* A data flow node that executes an operating system command, for instance by spawning a new
336+
* process.
337+
*
338+
* Extend this class to model new APIs. If you want to refine existing API models,
339+
* extend `SystemCommandExecution` instead.
340+
*/
341+
abstract class Range extends DataFlow::Node {
342+
/** Gets an argument to this execution that specifies the command or an argument to it. */
343+
abstract DataFlow::Node getAnArgument();
344+
345+
/** Holds if a shell interprets `arg`. */
346+
predicate isShellInterpreted(DataFlow::Node arg) { none() }
347+
}
348+
}

ql/lib/codeql/ruby/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
private import codeql.ruby.frameworks.ActionController
66
private import codeql.ruby.frameworks.ActiveRecord
77
private import codeql.ruby.frameworks.ActionView
8+
private import codeql.ruby.frameworks.StandardLibrary
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
private import codeql.ruby.AST
2+
private import codeql.ruby.Concepts
3+
private import codeql.ruby.DataFlow
4+
private import codeql.ruby.ApiGraphs
5+
private import codeql.ruby.dataflow.internal.DataFlowDispatch
6+
private import codeql.ruby.dataflow.internal.DataFlowImplCommon
7+
8+
/**
9+
* A system command executed via subshell literal syntax.
10+
* E.g.
11+
* ```ruby
12+
* `cat foo.txt`
13+
* %x(cat foo.txt)
14+
* %x[cat foo.txt]
15+
* %x{cat foo.txt}
16+
* %x/cat foo.txt/
17+
* ```
18+
*/
19+
class SubshellLiteralExecution extends SystemCommandExecution::Range {
20+
SubshellLiteral literal;
21+
22+
SubshellLiteralExecution() { this.asExpr().getExpr() = literal }
23+
24+
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = literal.getComponent(_) }
25+
26+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = getAnArgument() }
27+
}
28+
29+
/**
30+
* A system command executed via the `Kernel.system` method.
31+
* `Kernel.system` accepts three argument forms:
32+
* - A single string. If it contains no shell meta characters, keywords or builtins, it is executed directly in a subprocess.
33+
* Otherwise, it is executed in a subshell.
34+
* ```ruby
35+
* system("cat foo.txt | tail")
36+
* ```
37+
* - A command and one or more arguments.
38+
* The command is executed in a subprocess.
39+
* ```ruby
40+
* system("cat", "foo.txt")
41+
* ```
42+
* - An array containing the command name and argv[0], followed by zero or more arguments.
43+
* The command is executed in a subprocess.
44+
* ```ruby
45+
* system(["cat", "cat"], "foo.txt")
46+
* ```
47+
* In addition, `Kernel.system` accepts an optional environment hash as the first argument and and optional options hash as the last argument.
48+
* We don't yet distinguish between these arguments and the command arguments.
49+
* ```ruby
50+
* system({"FOO" => "BAR"}, "cat foo.txt | tail", {unsetenv_others: true})
51+
* ```
52+
* Ruby documentation: https://docs.ruby-lang.org/en/3.0.0/Kernel.html#method-i-system
53+
*/
54+
class KernelSystemCall extends SystemCommandExecution::Range {
55+
MethodCall methodCall;
56+
57+
KernelSystemCall() {
58+
methodCall.getMethodName() = "system" and
59+
this.asExpr().getExpr() = methodCall and
60+
// `Kernel.system` can be reached via `Kernel.system` or just `system`
61+
// (if there's no other method by the same name in scope).
62+
(
63+
this = API::getTopLevelMember("Kernel").getAMethodCall("system")
64+
or
65+
// we assume that if there's no obvious target for this method call, then it must refer to Kernel.system.
66+
not exists(DataFlowCallable method, DataFlowCall call |
67+
viableCallable(call) = method and call.getExpr() = methodCall
68+
)
69+
)
70+
}
71+
72+
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = methodCall.getAnArgument() }
73+
74+
override predicate isShellInterpreted(DataFlow::Node arg) {
75+
// Kernel.system invokes a subshell if you provide a single string as argument
76+
methodCall.getNumberOfArguments() = 1 and arg.asExpr().getExpr() = methodCall.getAnArgument()
77+
}
78+
}
79+
80+
/**
81+
* A system command executed via the `Kernel.exec` method.
82+
* `Kernel.exec` takes the same argument forms as `Kernel.system`. See `KernelSystemCall` for details.
83+
* Ruby documentation: https://docs.ruby-lang.org/en/3.0.0/Kernel.html#method-i-exec
84+
*/
85+
class KernelExecCall extends SystemCommandExecution::Range {
86+
MethodCall methodCall;
87+
88+
KernelExecCall() {
89+
methodCall.getMethodName() = "exec" and
90+
this.asExpr().getExpr() = methodCall and
91+
// `Kernel.exec` can be reached via `Kernel.exec`, `Process.exec` or just `exec`
92+
// (if there's no other method by the same name in scope).
93+
(
94+
this = API::getTopLevelMember("Kernel").getAMethodCall("exec")
95+
or
96+
this = API::getTopLevelMember("Process").getAMethodCall("exec")
97+
or
98+
// we assume that if there's no obvious target for this method call, then it must refer to Kernel.exec.
99+
not exists(DataFlowCallable method, DataFlowCall call |
100+
viableCallable(call) = method and call.getExpr() = methodCall
101+
)
102+
)
103+
}
104+
105+
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = methodCall.getAnArgument() }
106+
107+
override predicate isShellInterpreted(DataFlow::Node arg) {
108+
// Kernel.exec invokes a subshell if you provide a single string as argument
109+
methodCall.getNumberOfArguments() = 1 and arg.asExpr().getExpr() = methodCall.getAnArgument()
110+
}
111+
}
112+
113+
/**
114+
* A system command executed via the `Kernel.spawn` method.
115+
* `Kernel.spawn` takes the same argument forms as `Kernel.system`. See `KernelSystemCall` for details.
116+
* Ruby documentation: https://docs.ruby-lang.org/en/3.0.0/Kernel.html#method-i-spawn
117+
* TODO: document and handle the env and option arguments.
118+
* ```
119+
* spawn([env,] command... [,options]) → pid
120+
* ```
121+
*/
122+
class KernelSpawnCall extends SystemCommandExecution::Range {
123+
MethodCall methodCall;
124+
125+
KernelSpawnCall() {
126+
methodCall.getMethodName() = "spawn" and
127+
this.asExpr().getExpr() = methodCall and
128+
// `Kernel.spawn` can be reached via `Kernel.spawn`, `Process.spawn` or just `spawn`
129+
// (if there's no other method by the same name in scope).
130+
(
131+
this = API::getTopLevelMember("Kernel").getAMethodCall("spawn")
132+
or
133+
this = API::getTopLevelMember("Process").getAMethodCall("spawn")
134+
or
135+
not exists(DataFlowCallable method, DataFlowCall call |
136+
viableCallable(call) = method and call.getExpr() = methodCall
137+
)
138+
)
139+
}
140+
141+
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = methodCall.getAnArgument() }
142+
143+
override predicate isShellInterpreted(DataFlow::Node arg) {
144+
// Kernel.spawn invokes a subshell if you provide a single string as argument
145+
methodCall.getNumberOfArguments() = 1 and arg.asExpr().getExpr() = methodCall.getAnArgument()
146+
}
147+
}
148+
149+
class Open3Call extends SystemCommandExecution::Range {
150+
MethodCall methodCall;
151+
152+
Open3Call() {
153+
this.asExpr().getExpr() = methodCall and
154+
exists(string methodName |
155+
methodName in [
156+
"popen3", "popen2", "popen2e", "capture3", "capture2", "capture2e", "pipeline_rw",
157+
"pipeline_r", "pipeline_w", "pipeline_start", "pipeline"
158+
] and
159+
this = API::getTopLevelMember("Open3").getAMethodCall(methodName)
160+
)
161+
}
162+
163+
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = methodCall.getAnArgument() }
164+
165+
override predicate isShellInterpreted(DataFlow::Node arg) {
166+
// These Open3 methods invoke a subshell if you provide a single string as argument
167+
methodCall.getNumberOfArguments() = 1 and arg.asExpr().getExpr() = methodCall.getAnArgument()
168+
}
169+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
`echo foo`
2+
%x(echo foo)
3+
%x{echo foo}
4+
%x[echo foo]
5+
%x/echo foo/
6+
7+
system("echo foo")
8+
system("echo", "foo")
9+
system(["echo", "echo"], "foo")
10+
11+
system({"FOO" => "BAR"}, "echo foo")
12+
system({"FOO" => "BAR"}, "echo", "foo")
13+
system({"FOO" => "BAR"}, ["echo", "echo"], "foo")
14+
15+
system("echo foo", unsetenv_others: true)
16+
system("echo", "foo", unsetenv_others: true)
17+
system(["echo", "echo"], "foo", unsetenv_others: true)
18+
19+
system({"FOO" => "BAR"}, "echo foo", unsetenv_others: true)
20+
system({"FOO" => "BAR"}, "echo", "foo", unsetenv_others: true)
21+
system({"FOO" => "BAR"}, ["echo", "echo"], "foo", unsetenv_others: true)
22+
23+
exec("echo foo")
24+
exec("echo", "foo")
25+
exec(["echo", "echo"], "foo")
26+
27+
exec({"FOO" => "BAR"}, "echo foo")
28+
exec({"FOO" => "BAR"}, "echo", "foo")
29+
exec({"FOO" => "BAR"}, ["echo", "echo"], "foo")
30+
31+
exec("echo foo", unsetenv_others: true)
32+
exec("echo", "foo", unsetenv_others: true)
33+
exec(["echo", "echo"], "foo", unsetenv_others: true)
34+
35+
exec({"FOO" => "BAR"}, "echo foo", unsetenv_others: true)
36+
exec({"FOO" => "BAR"}, "echo", "foo", unsetenv_others: true)
37+
exec({"FOO" => "BAR"}, ["echo", "echo"], "foo", unsetenv_others: true)
38+
39+
spawn("echo foo")
40+
spawn("echo", "foo")
41+
spawn(["echo", "echo"], "foo")
42+
43+
spawn({"FOO" => "BAR"}, "echo foo")
44+
spawn({"FOO" => "BAR"}, "echo", "foo")
45+
spawn({"FOO" => "BAR"}, ["echo", "echo"], "foo")
46+
47+
spawn("echo foo", unsetenv_others: true)
48+
spawn("echo", "foo", unsetenv_others: true)
49+
spawn(["echo", "echo"], "foo", unsetenv_others: true)
50+
51+
spawn({"FOO" => "BAR"}, "echo foo", unsetenv_others: true)
52+
spawn({"FOO" => "BAR"}, "echo", "foo", unsetenv_others: true)
53+
spawn({"FOO" => "BAR"}, ["echo", "echo"], "foo", unsetenv_others: true)
54+
55+
Open3.popen3("echo foo")
56+
Open3.popen2("echo foo")
57+
Open3.popen2e("echo foo")
58+
Open3.capture3("echo foo")
59+
Open3.capture2("echo foo")
60+
Open3.capture2e("echo foo")
61+
Open3.pipeline_rw("echo foo")
62+
Open3.pipeline_r("echo foo")
63+
Open3.pipeline_w("echo foo")
64+
Open3.pipeline_start("echo foo")
65+
Open3.pipeline("echo foo")
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
subshellLiteralExecutions
2+
| CommandExecution.rb:1:1:1:10 | `echo foo` |
3+
| CommandExecution.rb:2:1:2:12 | `echo foo` |
4+
| CommandExecution.rb:3:1:3:12 | `echo foo` |
5+
| CommandExecution.rb:4:1:4:12 | `echo foo` |
6+
| CommandExecution.rb:5:1:5:12 | `echo foo` |
7+
kernelSystemCallExecutions
8+
| CommandExecution.rb:7:1:7:18 | call to system |
9+
| CommandExecution.rb:8:1:8:21 | call to system |
10+
| CommandExecution.rb:9:1:9:31 | call to system |
11+
| CommandExecution.rb:11:1:11:36 | call to system |
12+
| CommandExecution.rb:12:1:12:39 | call to system |
13+
| CommandExecution.rb:13:1:13:49 | call to system |
14+
| CommandExecution.rb:15:1:15:41 | call to system |
15+
| CommandExecution.rb:16:1:16:44 | call to system |
16+
| CommandExecution.rb:17:1:17:54 | call to system |
17+
| CommandExecution.rb:19:1:19:59 | call to system |
18+
| CommandExecution.rb:20:1:20:62 | call to system |
19+
| CommandExecution.rb:21:1:21:72 | call to system |
20+
kernelExecCallExecutions
21+
| CommandExecution.rb:23:1:23:16 | call to exec |
22+
| CommandExecution.rb:24:1:24:19 | call to exec |
23+
| CommandExecution.rb:25:1:25:29 | call to exec |
24+
| CommandExecution.rb:27:1:27:34 | call to exec |
25+
| CommandExecution.rb:28:1:28:37 | call to exec |
26+
| CommandExecution.rb:29:1:29:47 | call to exec |
27+
| CommandExecution.rb:31:1:31:39 | call to exec |
28+
| CommandExecution.rb:32:1:32:42 | call to exec |
29+
| CommandExecution.rb:33:1:33:52 | call to exec |
30+
| CommandExecution.rb:35:1:35:57 | call to exec |
31+
| CommandExecution.rb:36:1:36:60 | call to exec |
32+
| CommandExecution.rb:37:1:37:70 | call to exec |
33+
kernelSpawnCallExecutions
34+
| CommandExecution.rb:39:1:39:17 | call to spawn |
35+
| CommandExecution.rb:40:1:40:20 | call to spawn |
36+
| CommandExecution.rb:41:1:41:30 | call to spawn |
37+
| CommandExecution.rb:43:1:43:35 | call to spawn |
38+
| CommandExecution.rb:44:1:44:38 | call to spawn |
39+
| CommandExecution.rb:45:1:45:48 | call to spawn |
40+
| CommandExecution.rb:47:1:47:40 | call to spawn |
41+
| CommandExecution.rb:48:1:48:43 | call to spawn |
42+
| CommandExecution.rb:49:1:49:53 | call to spawn |
43+
| CommandExecution.rb:51:1:51:58 | call to spawn |
44+
| CommandExecution.rb:52:1:52:61 | call to spawn |
45+
| CommandExecution.rb:53:1:53:71 | call to spawn |
46+
open3CallExecutions
47+
| CommandExecution.rb:55:1:55:24 | call to popen3 |
48+
| CommandExecution.rb:56:1:56:24 | call to popen2 |
49+
| CommandExecution.rb:57:1:57:25 | call to popen2e |
50+
| CommandExecution.rb:58:1:58:26 | call to capture3 |
51+
| CommandExecution.rb:59:1:59:26 | call to capture2 |
52+
| CommandExecution.rb:60:1:60:27 | call to capture2e |
53+
| CommandExecution.rb:61:1:61:29 | call to pipeline_rw |
54+
| CommandExecution.rb:62:1:62:28 | call to pipeline_r |
55+
| CommandExecution.rb:63:1:63:28 | call to pipeline_w |
56+
| CommandExecution.rb:64:1:64:32 | call to pipeline_start |
57+
| CommandExecution.rb:65:1:65:26 | call to pipeline |
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import codeql.ruby.frameworks.StandardLibrary
2+
3+
query predicate subshellLiteralExecutions(SubshellLiteralExecution e) { any() }
4+
5+
query predicate kernelSystemCallExecutions(KernelSystemCall c) { any() }
6+
7+
query predicate kernelExecCallExecutions(KernelExecCall c) { any() }
8+
9+
query predicate kernelSpawnCallExecutions(KernelSpawnCall c) { any() }
10+
11+
query predicate open3CallExecutions(Open3Call c) { any() }

0 commit comments

Comments
 (0)